Write JSON output to file"
+ stdout.writeLine " --dry-run List scan targets without reading files"
+ stdout.writeLine " --quiet Suppress banner, show findings only"
+ stdout.writeLine " --verbose Show all scanned paths including empty modules"
+ stdout.writeLine " --help Show this help"
+ stdout.writeLine " --version Show version"
+ stdout.writeLine ""
+ stdout.writeLine ColorBold & "EXAMPLES:" & ColorReset
+ stdout.writeLine " " & BinaryName & " Scan current user"
+ stdout.writeLine " " & BinaryName & " --format json JSON output"
+ stdout.writeLine " " & BinaryName & " --modules ssh,git,cloud Scan specific modules"
+ stdout.writeLine " " & BinaryName & " --target /home/victim Scan another user"
+ stdout.writeLine " " & BinaryName & " --dry-run Preview scan paths"
+ stdout.writeLine ""
+ except CatchableError:
+ discard
+
+proc printVersion() =
+ try:
+ stdout.writeLine BinaryName & " " & AppVersion
+ except CatchableError:
+ discard
+
+proc parseModules*(input: string): seq[Category] =
+ result = @[]
+ let parts = input.split(",")
+ for part in parts:
+ let name = part.strip().toLowerAscii()
+ for cat in Category:
+ if ModuleNames[cat] == name:
+ result.add(cat)
+ break
+
+proc parseCli(): HarvestConfig =
+ result = defaultConfig()
+
+ var parser = initOptParser(
+ commandLineParams(),
+ shortNoVal = {'d', 'q', 'v', 'h'},
+ longNoVal = @["dry-run", "quiet", "verbose", "help", "version"]
+ )
+
+ while true:
+ parser.next()
+ case parser.kind
+ of cmdEnd:
+ break
+ of cmdShortOption, cmdLongOption:
+ case parser.key.toLowerAscii()
+ of "target", "t":
+ result.targetDir = parser.val
+ of "modules", "m":
+ result.enabledModules = parseModules(parser.val)
+ of "exclude", "e":
+ result.excludePatterns = parser.val.split(",")
+ of "format", "f":
+ case parser.val.toLowerAscii()
+ of "json":
+ result.outputFormat = fmtJson
+ of "both":
+ result.outputFormat = fmtBoth
+ else:
+ result.outputFormat = fmtTerminal
+ of "output", "o":
+ result.outputPath = parser.val
+ of "dry-run", "dry", "d":
+ result.dryRun = true
+ of "quiet", "q":
+ result.quiet = true
+ of "verbose", "v":
+ result.verbose = true
+ of "help", "h":
+ printHelp()
+ quit(0)
+ of "version":
+ printVersion()
+ quit(0)
+ else:
+ discard
+ of cmdArgument:
+ discard
+
+proc renderDryRun(conf: HarvestConfig) =
+ try:
+ stdout.writeLine ColorBold & "Dry run — scan targets:" & ColorReset
+ stdout.writeLine ""
+ for cat in conf.enabledModules:
+ stdout.writeLine " " & ColorCyan & ModuleNames[cat] & ColorReset & ": " & ModuleDescriptions[cat]
+ stdout.writeLine ""
+ stdout.writeLine ColorDim & " Target: " & conf.targetDir & ColorReset
+ stdout.writeLine ""
+ except CatchableError:
+ discard
+
+proc main() =
+ let conf = parseCli()
+
+ if conf.dryRun:
+ renderDryRun(conf)
+ quit(0)
+
+ var report = runCollectors(conf)
+
+ {.cast(raises: []).}:
+ report.metadata.timestamp = now().utc.format("yyyy-MM-dd'T'HH:mm:ss'Z'")
+
+ case conf.outputFormat
+ of fmtTerminal:
+ renderTerminal(report, conf.quiet, conf.verbose)
+ of fmtJson:
+ renderJson(report, conf.outputPath)
+ of fmtBoth:
+ renderTerminal(report, conf.quiet, conf.verbose)
+ renderJson(report, conf.outputPath)
+
+ var hasHighSeverity = false
+ for sev in [svCritical, svHigh]:
+ if report.summary[sev] > 0:
+ hasHighSeverity = true
+ break
+
+ if hasHighSeverity:
+ quit(1)
+ else:
+ quit(0)
+
+when isMainModule:
+ main()
diff --git a/PROJECTS/intermediate/credential-enumeration/src/output/json.nim b/PROJECTS/intermediate/credential-enumeration/src/output/json.nim
new file mode 100644
index 00000000..9a179341
--- /dev/null
+++ b/PROJECTS/intermediate/credential-enumeration/src/output/json.nim
@@ -0,0 +1,88 @@
+# ©AngelaMos | 2026
+# json.nim
+
+{.push raises: [].}
+
+import std/[json, options, tables]
+import ../types
+
+proc credentialToJson(cred: Credential): JsonNode =
+ result = newJObject()
+ {.cast(raises: []).}:
+ result["source"] = newJString(cred.source)
+ result["type"] = newJString(cred.credType)
+ result["preview"] = newJString(cred.preview)
+ let meta = newJObject()
+ for key, val in cred.metadata:
+ meta[key] = newJString(val)
+ result["metadata"] = meta
+
+proc findingToJson(f: Finding): JsonNode =
+ result = newJObject()
+ {.cast(raises: []).}:
+ result["path"] = newJString(f.path)
+ result["category"] = newJString($f.category)
+ result["severity"] = newJString($f.severity)
+ result["description"] = newJString(f.description)
+ result["permissions"] = newJString(f.permissions)
+ result["modified"] = newJString(f.modified)
+ result["size"] = newJInt(f.size)
+ if f.credential.isSome:
+ result["credential"] = credentialToJson(f.credential.get())
+
+proc collectorResultToJson(res: CollectorResult): JsonNode =
+ result = newJObject()
+ {.cast(raises: []).}:
+ result["name"] = newJString(res.name)
+ result["category"] = newJString($res.category)
+ let findings = newJArray()
+ for f in res.findings:
+ findings.add(findingToJson(f))
+ result["findings"] = findings
+ result["duration_ms"] = newJInt(res.durationMs)
+ let errors = newJArray()
+ for e in res.errors:
+ errors.add(newJString(e))
+ result["errors"] = errors
+
+proc reportToJson*(report: Report): JsonNode =
+ result = newJObject()
+ {.cast(raises: []).}:
+ let metadata = newJObject()
+ metadata["timestamp"] = newJString(report.metadata.timestamp)
+ metadata["target"] = newJString(report.metadata.target)
+ metadata["version"] = newJString(report.metadata.version)
+ metadata["duration_ms"] = newJInt(report.metadata.durationMs)
+ let modules = newJArray()
+ for m in report.metadata.modules:
+ modules.add(newJString(m))
+ metadata["modules"] = modules
+ result["metadata"] = metadata
+
+ let results = newJArray()
+ for res in report.results:
+ results.add(collectorResultToJson(res))
+ result["modules"] = results
+
+ let summary = newJObject()
+ for sev in Severity:
+ summary[$sev] = newJInt(report.summary[sev])
+ result["summary"] = summary
+
+proc renderJson*(report: Report, outputPath: string) =
+ let root = reportToJson(report)
+ let pretty = root.pretty(2)
+
+ if outputPath.len > 0:
+ try:
+ writeFile(outputPath, pretty & "\n")
+ except CatchableError as e:
+ try:
+ stderr.writeLine "Warning: could not write to " & outputPath & ": " & e.msg
+ except CatchableError:
+ discard
+
+ try:
+ stdout.writeLine pretty
+ except CatchableError:
+ discard
diff --git a/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim b/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim
new file mode 100644
index 00000000..630ba646
--- /dev/null
+++ b/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim
@@ -0,0 +1,203 @@
+# ©AngelaMos | 2026
+# terminal.nim
+
+{.push raises: [].}
+
+import std/[strutils, options]
+import ../types
+import ../config
+
+const
+ BoxWidth = 78
+ InnerWidth = BoxWidth - 2
+
+proc visualLen(s: string): int =
+ var i = 0
+ while i < s.len:
+ if s[i] == '\e':
+ while i < s.len and s[i] != 'm':
+ inc i
+ if i < s.len:
+ inc i
+ elif (s[i].ord and 0xC0) == 0x80:
+ inc i
+ else:
+ inc result
+ inc i
+
+proc truncateVisual(s: string, maxLen: int): string =
+ var vLen = 0
+ var i = 0
+ while i < s.len:
+ if s[i] == '\e':
+ let start = i
+ while i < s.len and s[i] != 'm':
+ inc i
+ if i < s.len:
+ inc i
+ result.add(s[start ..< i])
+ elif (s[i].ord and 0xC0) == 0x80:
+ result.add(s[i])
+ inc i
+ else:
+ if vLen >= maxLen - 3:
+ result.add("...")
+ return
+ result.add(s[i])
+ inc vLen
+ inc i
+
+proc writeBoxLine(content: string) =
+ try:
+ stdout.write content
+ let vLen = visualLen(content)
+ let pad = BoxWidth - vLen - 1
+ if pad > 0:
+ stdout.write " ".repeat(pad)
+ stdout.writeLine BoxVertical
+ except CatchableError:
+ discard
+
+proc sevBadge(sev: Severity): string =
+ SeverityColors[sev] & ColorBold & " " & SeverityLabels[sev] & " " & ColorReset
+
+proc boxLine(width: int): string =
+ BoxTopLeft & BoxHorizontal.repeat(width - 2) & BoxTopRight
+
+proc boxBottom(width: int): string =
+ BoxBottomLeft & BoxHorizontal.repeat(width - 2) & BoxBottomRight
+
+proc boxMid(width: int): string =
+ BoxTeeRight & BoxHorizontal.repeat(width - 2) & BoxTeeLeft
+
+proc renderBanner*(quiet: bool) =
+ if quiet:
+ return
+ try:
+ stdout.write ColorBoldRed
+ stdout.writeLine Banner
+ stdout.write ColorReset
+ stdout.writeLine ""
+ stdout.write " "
+ stdout.write ColorDim
+ stdout.write BannerTagline
+ stdout.write " v"
+ stdout.write AppVersion
+ stdout.writeLine ColorReset
+ stdout.writeLine ""
+ except CatchableError:
+ discard
+
+proc renderModuleHeader(name: string, desc: string, findingCount: int, durationMs: int64) =
+ try:
+ stdout.writeLine boxLine(BoxWidth)
+ let label = BoxVertical & " " & ColorBold & ColorCyan &
+ name.toUpperAscii() & ColorReset & ColorDim & " " & Arrow &
+ " " & desc & ColorReset
+ let stats = $findingCount & " findings" & ColorDim & " (" &
+ $durationMs & "ms)" & ColorReset
+ let usedWidth = 2 + name.len + 3 + desc.len
+ let statsVisual = visualLen(stats)
+ let gap = BoxWidth - usedWidth - statsVisual - 2
+ stdout.write label
+ if gap > 0:
+ stdout.write " ".repeat(gap)
+ else:
+ stdout.write " "
+ stdout.write stats
+ stdout.writeLine " " & BoxVertical
+ stdout.writeLine boxMid(BoxWidth)
+ except CatchableError:
+ discard
+
+proc renderFinding(f: Finding) =
+ let descLine = BoxVertical & " " & sevBadge(f.severity) & " " &
+ truncateVisual(f.description, InnerWidth - 14)
+ writeBoxLine(descLine)
+
+ var detail = BoxVertical & " " & ColorDim & f.path &
+ " [" & f.permissions & "]"
+ if f.modified != "unknown":
+ detail &= " mod:" & f.modified
+ detail &= ColorReset
+ writeBoxLine(detail)
+
+ if f.credential.isSome:
+ let cred = f.credential.get()
+ if cred.preview.len > 0:
+ let previewLine = BoxVertical & " " & ColorDim & Arrow &
+ " " & cred.preview & ColorReset
+ writeBoxLine(previewLine)
+
+proc renderModuleErrors(errors: seq[string]) =
+ if errors.len == 0:
+ return
+ for err in errors:
+ let errLine = BoxVertical & " " & ColorBoldRed & CrossMark &
+ ColorReset & " " & ColorDim & err & ColorReset
+ writeBoxLine(errLine)
+
+proc renderSummary(report: Report) =
+ try:
+ stdout.writeLine ""
+ stdout.writeLine boxLine(BoxWidth)
+ writeBoxLine(BoxVertical & " " & ColorBold & "SUMMARY" & ColorReset)
+ stdout.writeLine boxMid(BoxWidth)
+
+ var totalFindings = 0
+ for sev in Severity:
+ totalFindings += report.summary[sev]
+
+ let countLine = BoxVertical & " " & ColorBold & $totalFindings &
+ ColorReset & " findings across " & ColorBold &
+ $report.results.len & ColorReset & " modules" & ColorDim &
+ " (" & $report.metadata.durationMs & "ms)" & ColorReset
+ writeBoxLine(countLine)
+
+ var badgeLine = BoxVertical & " "
+ for sev in countdown(svCritical, svInfo):
+ let count = report.summary[sev]
+ if count > 0:
+ badgeLine &= sevBadge(sev) & " " & $count & " "
+ writeBoxLine(badgeLine)
+
+ stdout.writeLine boxBottom(BoxWidth)
+ stdout.writeLine ""
+ except CatchableError:
+ discard
+
+proc renderTerminal*(report: Report, quiet: bool, verbose: bool) =
+ renderBanner(quiet)
+
+ try:
+ if not quiet:
+ stdout.write ColorDim & " Target: " & ColorReset
+ stdout.writeLine report.metadata.target
+ stdout.write ColorDim & " Modules: " & ColorReset
+ stdout.writeLine report.metadata.modules.join(", ")
+ stdout.writeLine ""
+ except CatchableError:
+ discard
+
+ for res in report.results:
+ if res.findings.len == 0 and res.errors.len == 0 and not verbose:
+ continue
+
+ renderModuleHeader(
+ res.name,
+ ModuleDescriptions[res.category],
+ res.findings.len,
+ res.durationMs
+ )
+
+ for finding in res.findings:
+ renderFinding(finding)
+
+ renderModuleErrors(res.errors)
+
+ try:
+ stdout.writeLine boxBottom(BoxWidth)
+ except CatchableError:
+ discard
+
+ renderSummary(report)
diff --git a/PROJECTS/intermediate/credential-enumeration/src/runner.nim b/PROJECTS/intermediate/credential-enumeration/src/runner.nim
new file mode 100644
index 00000000..1b325cb4
--- /dev/null
+++ b/PROJECTS/intermediate/credential-enumeration/src/runner.nim
@@ -0,0 +1,56 @@
+# ©AngelaMos | 2026
+# runner.nim
+
+{.push raises: [].}
+
+import std/[monotimes, times]
+import types
+import config
+import collectors/ssh
+import collectors/git
+import collectors/cloud
+import collectors/browser
+import collectors/history
+import collectors/keyring
+import collectors/apptoken
+
+proc getCollector(cat: Category): CollectorProc =
+ case cat
+ of catBrowser: browser.collect
+ of catSsh: ssh.collect
+ of catCloud: cloud.collect
+ of catHistory: history.collect
+ of catKeyring: keyring.collect
+ of catGit: git.collect
+ of catApptoken: apptoken.collect
+
+proc runCollectors*(config: HarvestConfig): Report =
+ let start = getMonoTime()
+
+ var results: seq[CollectorResult] = @[]
+ var moduleNames: seq[string] = @[]
+
+ for cat in config.enabledModules:
+ moduleNames.add(ModuleNames[cat])
+ let collector = getCollector(cat)
+ let res = collector(config)
+ results.add(res)
+
+ let elapsed = getMonoTime() - start
+
+ var summary: array[Severity, int]
+ for res in results:
+ for finding in res.findings:
+ inc summary[finding.severity]
+
+ result = Report(
+ metadata: ReportMetadata(
+ timestamp: "",
+ target: config.targetDir,
+ version: AppVersion,
+ durationMs: elapsed.inMilliseconds,
+ modules: moduleNames
+ ),
+ results: results,
+ summary: summary
+ )
diff --git a/PROJECTS/intermediate/credential-enumeration/src/types.nim b/PROJECTS/intermediate/credential-enumeration/src/types.nim
new file mode 100644
index 00000000..05b69acc
--- /dev/null
+++ b/PROJECTS/intermediate/credential-enumeration/src/types.nim
@@ -0,0 +1,75 @@
+# ©AngelaMos | 2026
+# types.nim
+
+{.push raises: [].}
+
+import std/[options, tables]
+
+type
+ Severity* = enum
+ svInfo = "info"
+ svLow = "low"
+ svMedium = "medium"
+ svHigh = "high"
+ svCritical = "critical"
+
+ Category* = enum
+ catBrowser = "browser"
+ catSsh = "ssh"
+ catCloud = "cloud"
+ catHistory = "history"
+ catKeyring = "keyring"
+ catGit = "git"
+ catApptoken = "apptoken"
+
+ Credential* = object
+ source*: string
+ credType*: string
+ preview*: string
+ metadata*: Table[string, string]
+
+ Finding* = object
+ path*: string
+ category*: Category
+ severity*: Severity
+ description*: string
+ credential*: Option[Credential]
+ permissions*: string
+ modified*: string
+ size*: int64
+
+ CollectorResult* = object
+ name*: string
+ category*: Category
+ findings*: seq[Finding]
+ durationMs*: int64
+ errors*: seq[string]
+
+ ReportMetadata* = object
+ timestamp*: string
+ target*: string
+ version*: string
+ durationMs*: int64
+ modules*: seq[string]
+
+ Report* = object
+ metadata*: ReportMetadata
+ results*: seq[CollectorResult]
+ summary*: array[Severity, int]
+
+ OutputFormat* = enum
+ fmtTerminal = "terminal"
+ fmtJson = "json"
+ fmtBoth = "both"
+
+ HarvestConfig* = object
+ targetDir*: string
+ enabledModules*: seq[Category]
+ excludePatterns*: seq[string]
+ outputFormat*: OutputFormat
+ outputPath*: string
+ dryRun*: bool
+ quiet*: bool
+ verbose*: bool
+
+ CollectorProc* = proc(config: HarvestConfig): CollectorResult {.nimcall, raises: [].}
diff --git a/PROJECTS/intermediate/credential-enumeration/tests/docker/Dockerfile b/PROJECTS/intermediate/credential-enumeration/tests/docker/Dockerfile
new file mode 100644
index 00000000..74cf1690
--- /dev/null
+++ b/PROJECTS/intermediate/credential-enumeration/tests/docker/Dockerfile
@@ -0,0 +1,47 @@
+# ©AngelaMos | 2026
+# Dockerfile
+
+FROM nimlang/nim:2.2.0-alpine AS builder
+
+WORKDIR /build
+COPY src/ src/
+COPY config.nims .
+COPY credential-enumeration.nimble .
+
+RUN nim c -d:release --opt:size --passL:-static -o:/build/credenum src/harvester.nim && \
+ strip -s /build/credenum
+
+# ---------------------------------------------------------------------------
+
+FROM ubuntu:24.04
+
+RUN useradd -m -s /bin/bash testuser
+
+COPY --from=builder /build/credenum /usr/local/bin/credenum
+COPY tests/docker/planted/ /home/testuser/
+
+RUN chown -R testuser:testuser /home/testuser
+
+RUN chmod 0644 /home/testuser/.ssh/id_rsa_unprotected && \
+ chmod 0600 /home/testuser/.ssh/id_ed25519_encrypted && \
+ chmod 0700 /home/testuser/.ssh && \
+ chmod 0644 /home/testuser/.ssh/config && \
+ chmod 0644 /home/testuser/.ssh/authorized_keys && \
+ chmod 0644 /home/testuser/.ssh/known_hosts
+
+RUN chmod 0644 /home/testuser/.aws/credentials && \
+ chmod 0644 /home/testuser/.git-credentials && \
+ chmod 0644 /home/testuser/.pgpass && \
+ chmod 0644 /home/testuser/.my.cnf && \
+ chmod 0644 /home/testuser/.netrc && \
+ chmod 0644 /home/testuser/.npmrc && \
+ chmod 0644 /home/testuser/.pypirc && \
+ chmod 0644 /home/testuser/.vault-token
+
+COPY tests/docker/validate.sh /validate.sh
+RUN chmod +x /validate.sh
+
+USER testuser
+WORKDIR /home/testuser
+
+CMD ["/validate.sh"]
diff --git a/PROJECTS/intermediate/credential-enumeration/tests/docker/validate.sh b/PROJECTS/intermediate/credential-enumeration/tests/docker/validate.sh
new file mode 100644
index 00000000..13aa54e7
--- /dev/null
+++ b/PROJECTS/intermediate/credential-enumeration/tests/docker/validate.sh
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+# ©AngelaMos | 2026
+# validate.sh
+
+set -euo pipefail
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+PASS=0
+FAIL=0
+
+check() {
+ local label="$1"
+ local pattern="$2"
+ if echo "$OUTPUT" | grep -qi "$pattern"; then
+ echo -e " ${GREEN}+${NC} $label"
+ PASS=$((PASS + 1))
+ else
+ echo -e " ${RED}x${NC} $label"
+ FAIL=$((FAIL + 1))
+ fi
+}
+
+echo -e "\n${BOLD}Running credenum...${NC}\n"
+
+OUTPUT=$(credenum --target /home/testuser --format json 2>&1) || true
+
+echo -e "${BOLD}Terminal output:${NC}\n"
+credenum --target /home/testuser 2>&1 || true
+
+echo -e "\n${BOLD}Validating findings across all 7 categories...${NC}\n"
+
+echo -e "${BOLD}[ssh]${NC}"
+check "SSH unprotected private key" "no passphrase"
+check "SSH encrypted private key" "passphrase-protected"
+check "SSH config weak settings" "PasswordAuthentication"
+check "SSH authorized keys" "authorized public keys"
+check "SSH known hosts" "known hosts"
+
+echo -e "\n${BOLD}[cloud]${NC}"
+check "AWS credentials with static keys" "static keys"
+check "AWS config profiles" "profiles"
+check "GCP service account" "service_account"
+check "Kubernetes config" "contexts"
+
+echo -e "\n${BOLD}[browser]${NC}"
+check "Firefox stored logins" "Firefox stored logins"
+check "Firefox cookies" "Firefox cookies"
+check "Firefox key database" "Firefox key"
+check "Chromium login data" "google-chrome.*login"
+
+echo -e "\n${BOLD}[history]${NC}"
+check "History secret pattern" "Secret in shell history"
+check "Sensitive command (curl auth)" "curl.*authoriz"
+check "Sensitive command (sshpass)" "sshpass"
+check "Environment file" "Environment file"
+
+echo -e "\n${BOLD}[keyring]${NC}"
+check "GNOME Keyring" "GNOME Keyring"
+check "KeePass database" "KeePass"
+check "Password store" "password-store"
+
+echo -e "\n${BOLD}[git]${NC}"
+check "Git credentials plaintext" "Plaintext Git credential"
+check "Git credential helper" "credential helper"
+check "GitHub token" "GitHub.*token"
+
+echo -e "\n${BOLD}[apptoken]${NC}"
+check "PostgreSQL pgpass" "PostgreSQL"
+check "MySQL config" "MySQL"
+check "Docker registry auth" "Docker.*auth"
+check "Netrc credential file" "Netrc credential"
+check "npm auth token" "npm registry"
+check "PyPI credentials" "PyPI.*credentials"
+check "GitHub CLI OAuth token" "GitHub CLI"
+check "Vault token" "Vault token"
+
+echo ""
+echo -e "${BOLD}Results: ${GREEN}${PASS} passed${NC}, ${RED}${FAIL} failed${NC}"
+echo ""
+
+if [ "$FAIL" -gt 0 ]; then
+ echo -e "${RED}VALIDATION FAILED${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}ALL CHECKS PASSED${NC}"
diff --git a/PROJECTS/intermediate/credential-enumeration/tests/test_all b/PROJECTS/intermediate/credential-enumeration/tests/test_all
new file mode 100755
index 00000000..93650f2d
Binary files /dev/null and b/PROJECTS/intermediate/credential-enumeration/tests/test_all differ
diff --git a/PROJECTS/intermediate/credential-enumeration/tests/test_all.nim b/PROJECTS/intermediate/credential-enumeration/tests/test_all.nim
new file mode 100644
index 00000000..8fb5df05
--- /dev/null
+++ b/PROJECTS/intermediate/credential-enumeration/tests/test_all.nim
@@ -0,0 +1,209 @@
+# ©AngelaMos | 2026
+# test_all.nim
+
+import std/[unittest, strutils]
+import types
+import collectors/base
+import collectors/ssh
+import collectors/history
+import harvester
+
+suite "redactValue":
+ test "short value fully redacted":
+ check redactValue("abc", 4) == "***"
+
+ test "value longer than showChars":
+ check redactValue("mysecret", 4) == "myse****"
+
+ test "exact showChars length":
+ check redactValue("abcd", 4) == "****"
+
+ test "empty string":
+ check redactValue("", 4) == ""
+
+suite "isPrivateKey":
+ test "OpenSSH key":
+ check isPrivateKey("-----BEGIN OPENSSH PRIVATE KEY-----\ndata")
+
+ test "RSA key":
+ check isPrivateKey("-----BEGIN RSA PRIVATE KEY-----\ndata")
+
+ test "ECDSA key":
+ check isPrivateKey("-----BEGIN EC PRIVATE KEY-----\ndata")
+
+ test "DSA key":
+ check isPrivateKey("-----BEGIN DSA PRIVATE KEY-----\ndata")
+
+ test "generic PKCS8 key":
+ check isPrivateKey("-----BEGIN PRIVATE KEY-----\ndata")
+
+ test "public key rejected":
+ check isPrivateKey("-----BEGIN PUBLIC KEY-----\ndata") == false
+
+ test "random text rejected":
+ check isPrivateKey("this is not a key") == false
+
+ test "empty string rejected":
+ check isPrivateKey("") == false
+
+suite "isEncrypted":
+ test "ENCRYPTED marker":
+ check isEncrypted(
+ "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\ndata"
+ )
+
+ test "bcrypt marker":
+ check isEncrypted(
+ "-----BEGIN OPENSSH PRIVATE KEY-----\nbcrypt\ndata"
+ )
+
+ test "aes256-ctr marker":
+ check isEncrypted("data with aes256-ctr in it")
+
+ test "unencrypted key":
+ check isEncrypted(
+ "-----BEGIN OPENSSH PRIVATE KEY-----\nAAAAB3NzaC1\ndata"
+ ) == false
+
+suite "matchesSecretPattern":
+ test "export with KEY=":
+ check matchesSecretPattern("export API_KEY=some_value")
+
+ test "export with SECRET=":
+ check matchesSecretPattern(
+ "export AWS_SECRET_ACCESS_KEY=abc123"
+ )
+
+ test "bare TOKEN= at start":
+ check matchesSecretPattern("TOKEN=abcdef12345")
+
+ test "bare PASSWORD=":
+ check matchesSecretPattern("PASSWORD=hunter2")
+
+ test "non-secret assignment":
+ check matchesSecretPattern("export PATH=/usr/bin") == false
+
+ test "ordinary command":
+ check matchesSecretPattern("ls -la /tmp") == false
+
+ test "empty string":
+ check matchesSecretPattern("") == false
+
+suite "matchesCommandPattern":
+ test "curl with auth header":
+ check matchesCommandPattern(
+ "curl -H \"Authorization: Bearer token\" https://api.example.com"
+ )
+
+ test "curl with lowercase -h auth":
+ check matchesCommandPattern(
+ "curl -h \"authorization: bearer token\" https://api.example.com"
+ )
+
+ test "curl with -u flag":
+ check matchesCommandPattern(
+ "curl -u user:pass https://api.example.com"
+ )
+
+ test "wget with authorization header":
+ check matchesCommandPattern(
+ "wget --header=\"Authorization: Basic abc\" https://example.com"
+ )
+
+ test "wget with password":
+ check matchesCommandPattern(
+ "wget --password=secret https://files.example.com/data.zip"
+ )
+
+ test "mysql with -p flag":
+ check matchesCommandPattern("mysql -u root -psecret mydb")
+
+ test "psql with password":
+ check matchesCommandPattern(
+ "psql password=secret host=db.example.com"
+ )
+
+ test "sshpass command":
+ check matchesCommandPattern("sshpass -p 'mypass' ssh user@host")
+
+ test "safe curl rejected":
+ check matchesCommandPattern("curl https://example.com") == false
+
+ test "safe git command rejected":
+ check matchesCommandPattern("git push origin main") == false
+
+ test "empty string rejected":
+ check matchesCommandPattern("") == false
+
+suite "matchesExclude":
+ test "exact filename match":
+ check matchesExclude("/home/user/.env", @[".env"])
+
+ test "directory segment match":
+ check matchesExclude("/home/user/.git/config", @[".git"])
+
+ test "no false positive on partial name":
+ check matchesExclude(
+ "/home/user/.venv/lib/site.py", @[".env"]
+ ) == false
+
+ test "no match on embedded substring":
+ check matchesExclude(
+ "/home/user/environment/data", @[".env"]
+ ) == false
+
+ test "empty patterns":
+ check matchesExclude("/home/user/.env", @[]) == false
+
+suite "permissionSeverity":
+ test "returns svInfo for unreadable path":
+ check permissionSeverity("/nonexistent/path/abc123") == svInfo
+
+suite "parseModules":
+ test "single module":
+ check parseModules("ssh") == @[catSsh]
+
+ test "multiple modules":
+ let mods = parseModules("ssh,git,cloud")
+ check mods.len == 3
+ check mods.contains(catSsh)
+ check mods.contains(catGit)
+ check mods.contains(catCloud)
+
+ test "with whitespace":
+ let mods = parseModules(" browser , keyring ")
+ check mods.len == 2
+ check mods.contains(catBrowser)
+ check mods.contains(catKeyring)
+
+ test "all modules":
+ let mods = parseModules(
+ "browser,ssh,cloud,history,keyring,git,apptoken"
+ )
+ check mods.len == 7
+
+ test "empty string":
+ check parseModules("").len == 0
+
+ test "unknown module ignored":
+ check parseModules("fake,nonexistent").len == 0
+
+suite "redactLine":
+ test "export with quoted value":
+ let got = redactLine("export KEY=\"secret\"")
+ check got.contains("KEY=")
+ check got.contains("\"") == false
+
+ test "export with unquoted value":
+ let got = redactLine("export API_KEY=mysecretvalue")
+ check got.contains("API_KEY=")
+ check got.contains("myse")
+ check got.contains("cretvalue") == false
+
+ test "no equals sign":
+ check redactLine("no assignment here") == "no assignment here"
+
+ test "single-quoted value":
+ let got = redactLine("export TOKEN='abcdefgh'")
+ check got.contains("TOKEN=")
+ check got.contains("'") == false
diff --git a/README.md b/README.md
index 17f91671..fe0d1dca 100644
--- a/README.md
+++ b/README.md
@@ -20,16 +20,16 @@
-Made possible by CertGames
+Made possible by CertGames
View Complete Projects:
-Currently building: Subdomain Takeover Scanner
+Currently building: Binary Analysis Tool
---
@@ -68,7 +68,7 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
| **[SSH Brute Force Detector](./SYNOPSES/beginner/SSH.Brute.Force.Detector.md)**
Monitor and block SSH attacks |    | Log parsing • Attack detection • Firewall automation
[Learn More](./SYNOPSES/beginner/SSH.Brute.Force.Detector.md) |
| **[Simple C2 Beacon](./PROJECTS/beginner/c2-beacon)**
Command and Control beacon/server |      | C2 architecture • MITRE ATT&CK • WebSocket protocol • XOR encoding
[Source Code](./PROJECTS/beginner/c2-beacon) \| [Docs](./PROJECTS/beginner/c2-beacon/learn) |
| **[Base64 Encoder/Decoder](./SYNOPSES/beginner/Base64.Encoder.Decoder.md)**
Multi-format encoding tool |    | Base64/32 encoding • URL encoding • Auto-detection
[Source Code](./PROJECTS/beginner/base64-tool) \| [Docs](./PROJECTS/beginner/base64-tool/learn) |
-| **[Linux CIS Hardening Auditor](./PROJECTS/beginner/linux-cis-hardening-auditor)**
CIS benchmark compliance checker |    | CIS benchmarks • System hardening • Compliance scoring • Shell scripting
[Source Code](./PROJECTS/beginner/linux-cis-hardening-auditor) \| [Docs](./PROJECTS/beginner/linux-cis-hardening-auditor/learn) |
+| **[Linux CIS Hardening Auditor](./SYNOPSES/beginner/Linux.CIS.Hardening.Auditor.md)**
CIS benchmark compliance checker |    | CIS benchmarks • System hardening • Compliance scoring • Shell scripting
[Learn More](./SYNOPSES/beginner/Linux.CIS.Hardening.Auditor.md) |
| **[Systemd Persistence Scanner](./SYNOPSES/beginner/Systemd.Persistence.Scanner.md)**
Hunt Linux persistence mechanisms |    | Persistence techniques • Systemd internals • Cron analysis • Threat hunting
[Learn More](./SYNOPSES/beginner/Systemd.Persistence.Scanner.md) |
| **[Linux eBPF Security Tracer](./SYNOPSES/beginner/Linux.eBPF.Security.Tracer.md)**
Real-time syscall tracing with eBPF |     | eBPF programs • Syscall tracing • BCC framework • Security observability
[Learn More](./SYNOPSES/beginner/Linux.eBPF.Security.Tracer.md) |
| **[Trojan Application Builder](./SYNOPSES/beginner/Trojan.Application.Builder.md)**
Educational malware lifecycle demo |    | Trojan anatomy • Data exfiltration • File encryption • Attack lifecycle
[Learn More](./SYNOPSES/beginner/Trojan.Application.Builder.md) |
@@ -88,8 +88,8 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
| **[Secrets Scanner](./PROJECTS/intermediate/secrets-scanner)**
Scan codebases and git history for leaked secrets |    | Secret detection • Shannon entropy • HIBP k-anonymity • SARIF output
[Source Code](./PROJECTS/intermediate/secrets-scanner) \| [Docs](./PROJECTS/intermediate/secrets-scanner/learn) |
| **[API Security Scanner](./PROJECTS/intermediate/api-security-scanner)**
Enterprise API vulnerability scanner |      | OWASP API Top 10 • ML fuzzing • GraphQL/SOAP testing
[Source Code](./PROJECTS/intermediate/api-security-scanner) \| [Docs](./PROJECTS/intermediate/api-security-scanner/learn) |
| **[Wireless Deauth Detector](./SYNOPSES/intermediate/Wireless.Deauth.Detector.md)**
Monitor WiFi deauth attacks |    | Wireless security • Packet sniffing • Attack detection
[Learn More](./SYNOPSES/intermediate/Wireless.Deauth.Detector.md) |
-| **[Credential Harvester](./SYNOPSES/intermediate/Nim.Credential.Harvester.md)**
Post-exploitation credential collection |    | Credential extraction • Browser forensics • Red team tooling • Nim language
[Learn More](./SYNOPSES/intermediate/Nim.Credential.Harvester.md) |
-| **[Binary Analysis Tool](./PROJECTS/intermediate/binary-analysis-tool)**
Disassemble and analyze executables |    | Binary analysis • String extraction • Malware detection
[Source Code](./PROJECTS/intermediate/binary-analysis-tool) \| [Docs](./PROJECTS/intermediate/binary-analysis-tool/learn) |
+| **[Credential Enumeration](./SYNOPSES/intermediate/Nim.Credential.Enumeration.md)**
Post-exploitation credential collection |    | Credential extraction • Browser forensics • Red team tooling • Nim language
[Learn More](./SYNOPSES/intermediate/Nim.Credential.Enumeration.md) |
+| **[Binary Analysis Tool](./SYNOPSES/intermediate/Binary.Analysis.Tool.md)**
Disassemble and analyze executables |    | Binary analysis • String extraction • Malware detection
[Learn More](./SYNOPSES/intermediate/Binary.Analysis.Tool.md) |
| **[Chaos Engineering Security Tool](./SYNOPSES/intermediate/Chaos.Engineering.Security.Tool.md)**
Inject security failures to test resilience |    | Chaos engineering • Security resilience • Credential spraying • Auth testing
[Learn More](./SYNOPSES/intermediate/Chaos.Engineering.Security.Tool.md) |
| **[Credential Rotation Enforcer](./SYNOPSES/intermediate/Credential.Rotation.Enforcer.md)**
Track and enforce credential rotation policies |    | Credential hygiene • Secret rotation • Compliance dashboards • API integration
[Learn More](./SYNOPSES/intermediate/Credential.Rotation.Enforcer.md) |
| **[Race Condition Exploiter](./SYNOPSES/intermediate/Race.Condition.Exploiter.md)**
TOCTOU race condition attack & defense lab |     | TOCTOU attacks • Double-spend bugs • Concurrent exploitation • Race visualization
[Learn More](./SYNOPSES/intermediate/Race.Condition.Exploiter.md) |
@@ -141,4 +141,4 @@ Tools, courses, certifications, communities, and frameworks for cybersecurity pr
## License
-AGPL 3.0
+AGPL 3.0
\ No newline at end of file
diff --git a/ROADMAPS/APPLICATION-SECURITY.md b/ROADMAPS/APPLICATION-SECURITY.md
index 6d82b957..fa6e2ff2 100644
--- a/ROADMAPS/APPLICATION-SECURITY.md
+++ b/ROADMAPS/APPLICATION-SECURITY.md
@@ -8,6 +8,10 @@ Application Security professionals work with development teams to build secure s
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -313,4 +317,8 @@ Build application security skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/CLOUD-SECURITY-ENGINEER.md b/ROADMAPS/CLOUD-SECURITY-ENGINEER.md
index 617499f7..d72c06a0 100644
--- a/ROADMAPS/CLOUD-SECURITY-ENGINEER.md
+++ b/ROADMAPS/CLOUD-SECURITY-ENGINEER.md
@@ -8,6 +8,10 @@ Cloud Security Engineers design, implement, and maintain security controls for c
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -241,4 +245,8 @@ Build cloud security skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/GRC-ANALYST.md b/ROADMAPS/GRC-ANALYST.md
index ead6ce9e..ffeeb868 100644
--- a/ROADMAPS/GRC-ANALYST.md
+++ b/ROADMAPS/GRC-ANALYST.md
@@ -8,6 +8,10 @@ GRC Analysts ensure organizations meet security standards and regulatory require
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -258,4 +262,8 @@ Understand GRC through practical application:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/INCIDENT-RESPONDER.md b/ROADMAPS/INCIDENT-RESPONDER.md
index b080b108..5e6d9f7f 100644
--- a/ROADMAPS/INCIDENT-RESPONDER.md
+++ b/ROADMAPS/INCIDENT-RESPONDER.md
@@ -8,6 +8,10 @@ Incident Responders are the front-line defenders when security incidents occur.
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -200,4 +204,8 @@ Practice incident response skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/NETWORK-ENGINEER.md b/ROADMAPS/NETWORK-ENGINEER.md
index aae1e30e..de9452b6 100644
--- a/ROADMAPS/NETWORK-ENGINEER.md
+++ b/ROADMAPS/NETWORK-ENGINEER.md
@@ -8,6 +8,10 @@ Network Engineers with security focus build and maintain secure network infrastr
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -288,4 +292,8 @@ Build network security skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/PENTESTER.md b/ROADMAPS/PENTESTER.md
index 29512b1e..2115c116 100644
--- a/ROADMAPS/PENTESTER.md
+++ b/ROADMAPS/PENTESTER.md
@@ -8,6 +8,10 @@ Penetration testers ethically hack systems to identify vulnerabilities before ma
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -170,4 +174,8 @@ Build pentesting skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/README.md b/ROADMAPS/README.md
index 114e53b9..4841bf39 100644
--- a/ROADMAPS/README.md
+++ b/ROADMAPS/README.md
@@ -66,6 +66,10 @@ Structured certification paths for different cybersecurity career tracks. Each r
---
+> **Want 1-on-1 guidance?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## How to Use These Roadmaps
1. **Choose Your Path** - Select the role that aligns with your career goals
@@ -126,6 +130,12 @@ Structured certification paths for different cybersecurity career tracks. Each r
---
+---
+
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
**Last Updated:** January 2026
[Back to Main README](../README.md)
diff --git a/ROADMAPS/SECURITY-ARCHITECT.md b/ROADMAPS/SECURITY-ARCHITECT.md
index d0bb6be0..1a39daa4 100644
--- a/ROADMAPS/SECURITY-ARCHITECT.md
+++ b/ROADMAPS/SECURITY-ARCHITECT.md
@@ -8,6 +8,10 @@ Security Architects design the overall security infrastructure for organizations
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -231,4 +235,8 @@ Understand architecture through implementation:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/SECURITY-ENGINEER.md b/ROADMAPS/SECURITY-ENGINEER.md
index 6ed54a8d..0c3d793d 100644
--- a/ROADMAPS/SECURITY-ENGINEER.md
+++ b/ROADMAPS/SECURITY-ENGINEER.md
@@ -8,6 +8,10 @@ Security Engineers build and maintain the technical security infrastructure that
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -176,4 +180,8 @@ Build security engineering skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/SOC-ANALYST.md b/ROADMAPS/SOC-ANALYST.md
index b6a3d906..403d2d83 100644
--- a/ROADMAPS/SOC-ANALYST.md
+++ b/ROADMAPS/SOC-ANALYST.md
@@ -8,6 +8,10 @@ SOC Analysts monitor, detect, investigate, and respond to cybersecurity threats.
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -139,4 +143,8 @@ Practice SOC skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/ROADMAPS/THREAT-INTELLIGENCE-ANALYST.md b/ROADMAPS/THREAT-INTELLIGENCE-ANALYST.md
index 8276134f..b24b3188 100644
--- a/ROADMAPS/THREAT-INTELLIGENCE-ANALYST.md
+++ b/ROADMAPS/THREAT-INTELLIGENCE-ANALYST.md
@@ -8,6 +8,10 @@ Threat Intelligence Analysts research adversaries, analyze attack patterns, and
---
+> **Want 1-on-1 guidance through this path?** I offer a mentorship program where I personally help you get certified, build real projects for your GitHub, rewrite your resume, and land your first cybersecurity role. **[Learn more](https://certgames.com/mentorship)**
+
+---
+
## Certification Path
| Level | Certification | Organization | Link |
@@ -259,4 +263,8 @@ Build threat intelligence skills with these projects:
---
+> **This is a lot to tackle alone.** If you want someone guiding you through the certifications, building your projects, and getting your resume right — my 1-on-1 mentorship covers the full process for 90 days. **[certgames.com/mentorship](https://certgames.com/mentorship)**
+
+---
+
[Back to All Roadmaps](./README.md)
diff --git a/docs/plans/2026-04-01-credential-enumeration-audit-fixes.md b/docs/plans/2026-04-01-credential-enumeration-audit-fixes.md
new file mode 100644
index 00000000..e4545b73
--- /dev/null
+++ b/docs/plans/2026-04-01-credential-enumeration-audit-fixes.md
@@ -0,0 +1,591 @@
+# Credential Enumeration Audit
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans
+> to implement this plan task-by-task.
+
+**Goal:** Address all gaps identified in the audit.
+
+**Architecture:** All changes are modifications to existing files unless noted.
+
+**Tech Stack:** Nim 2.2+, Docker, Bash (Justfile)
+
+---
+
+## Impression
+
+Solid architecture for a Nim CLI tool — clean type hierarchy, consistent
+`{.push raises: [].}` discipline, well-structured collector pattern. The
+bones are genuinely good. But two of the command-detection patterns silently
+match nothing, the terminal box renderer computes stats it never prints,
+and the only test mechanism (Docker) can't actually build because the
+Justfile passes the wrong build context. The tool scans 7 credential
+categories competently but misses several high-value targets (.netrc,
+npm/pip tokens, Terraform, Vault) that a real post-access operator would
+check first.
+
+## Project Assessment
+
+**Type:** Rule-based credential detection CLI tool (post-access)
+**Primary Axis:** Completeness — weighted 65/35 over code quality
+**Why:** A scanner's value is directly proportional to what it catches.
+Missing a credential category is a harder failure than a rendering bug.
+
+## Findings
+
+### Finding 1: Docker test build context is wrong — entire test pipeline broken
+**Severity:** CRITICAL
+**Axis:** Code Quality
+**Files:** Justfile:88-89, tests/docker/Dockerfile:1-12
+
+**Issue:** The Justfile recipe `docker-build` runs
+`docker build -t credenum-test tests/docker`, setting the build context to
+`tests/docker/`. But the Dockerfile's first stage copies `src/`, `config.nims`,
+and `credential-enumeration.nimble` from the build context root — none of which
+exist under `tests/docker/`. The build fails immediately with
+"COPY failed: file not found in build context."
+
+**Proof:** The Dockerfile contains:
+```dockerfile
+COPY src/ src/
+COPY config.nims .
+COPY credential-enumeration.nimble .
+```
+With context `tests/docker/`, Docker looks for `tests/docker/src/`,
+`tests/docker/config.nims`, `tests/docker/credential-enumeration.nimble`.
+None exist — `find tests/docker/ -name "config.nims"` returns nothing.
+The only test mechanism for this project has never run successfully with
+this Justfile recipe.
+
+**Proof Check:** Confidence: HIGH — Docker build context semantics are deterministic;
+this is not a maybe.
+
+**Fix:**
+`Justfile:88-89` — change the docker-build recipe to use the project root as context:
+```just
+[group('test')]
+docker-build:
+ docker build -t credenum-test -f tests/docker/Dockerfile .
+```
+And update `docker-test` accordingly (it depends on docker-build, so no change needed
+there since it just `docker run`s the image).
+
+**Test:**
+```bash
+just docker-build
+```
+
+---
+
+### Finding 2: matchesCommandPattern has case mismatch — 2/7 patterns are dead code
+**Severity:** CRITICAL
+**Axis:** Code Quality
+**Files:** src/collectors/history.nim:38-54, src/config.nim:120-128
+
+**Issue:** `matchesCommandPattern` lowercases the input line (`line.toLowerAscii()`)
+then searches for pattern fragments that contain uppercase characters. Two patterns
+are affected:
+
+- `"curl.*-H.*[Aa]uthoriz"` splits into `["curl", "-H", "[Aa]uthoriz"]` —
+ `-H` (uppercase) will never be found in a lowercased string, and
+ `[Aa]uthoriz` is treated as a literal (not a character class)
+- `"wget.*--header.*[Aa]uthoriz"` splits into `["wget", "--header", "[Aa]uthoriz"]` —
+ `[Aa]uthoriz` is literal and will never appear in real history
+
+This means `curl -H "Authorization: Bearer ..."` commands in shell history
+are silently missed — one of the most common credential-leaking patterns.
+
+**Proof:** Trace through `matchesCommandPattern` with input
+`curl -H "Authorization: Bearer token" https://api.example.com`:
+1. `lower` = `curl -h "authorization: bearer token" https://api.example.com`
+2. Pattern `"curl.*-H.*[Aa]uthoriz"` → parts = `["curl", "-H", "[Aa]uthoriz"]`
+3. `lower.find("curl")` → found at 0
+4. `lower.find("-H")` → NOT FOUND (lowercase string has `-h`, not `-H`)
+5. `allFound = false` → returns false
+
+The pattern never matches. The planted test data in `.bash_history` line 4
+has `curl -H "Authorization: ..."` which should trigger this pattern but
+the validate.sh check labeled "Sensitive command" passes only because
+OTHER patterns (like `sshpass`, `mysql.*-p`) produce matches.
+
+**Proof Check:** Confidence: HIGH — Nim's `find` is case-sensitive by default;
+this is deterministic.
+
+**Fix:**
+`src/config.nim:120-128` — lowercase all pattern fragments:
+```nim
+HistoryCommandPatterns* = [
+ "curl.*-h.*authoriz",
+ "curl.*-u ",
+ "wget.*--header.*authoriz",
+ "wget.*--password",
+ "mysql.*-p",
+ "psql.*password",
+ "sshpass"
+]
+```
+
+**Test:**
+Add a Docker test assertion that specifically validates curl -H Authorization
+detection. After fix, run `just docker-test`.
+
+---
+
+### Finding 3: Module header stats computed but never rendered
+**Severity:** MAJOR
+**Axis:** Code Quality
+**Files:** src/output/terminal.nim:40-57
+
+**Issue:** `renderModuleHeader` computes a `stats` string containing
+the finding count and duration, but the padding calculation
+`padLen - stats.len + stats.len` simplifies to just `padLen` — then
+writes padding spaces without ever writing `stats` to stdout.
+The finding count and per-module duration are silently dropped from output.
+
+**Proof:** The arithmetic:
+```nim
+let stats = $findingCount & " findings" & ColorDim & " (" & $durationMs & "ms)" & ColorReset
+let padLen = 76 - name.len - desc.len - 5
+if padLen > 0:
+ stdout.write " ".repeat(padLen - stats.len + stats.len) # = " ".repeat(padLen)
+stdout.writeLine " " & BoxVertical
+```
+`stats` is never passed to `stdout.write`. The line is equivalent to
+`stdout.write " ".repeat(padLen)` followed by the box border — no stats
+anywhere.
+
+**Proof Check:** Confidence: HIGH — the variable is computed and never
+appears in any write call in the function.
+
+**Fix:**
+`src/output/terminal.nim:51-55` — compute visual width (excluding ANSI codes),
+pad to fill the box, then write stats:
+```nim
+proc visualLen(s: string): int =
+ var i = 0
+ while i < s.len:
+ if s[i] == '\e':
+ while i < s.len and s[i] != 'm':
+ inc i
+ inc i
+ else:
+ inc result
+ inc i
+
+proc renderModuleHeader(name: string, desc: string, findingCount: int, durationMs: int64) =
+ try:
+ stdout.writeLine boxLine(78)
+ stdout.write BoxVertical & " "
+ stdout.write ColorBold & ColorCyan
+ stdout.write name.toUpperAscii()
+ stdout.write ColorReset
+ stdout.write ColorDim
+ stdout.write " " & Arrow & " " & desc
+ stdout.write ColorReset
+
+ let stats = $findingCount & " findings" & ColorDim & " (" & $durationMs & "ms)" & ColorReset
+ let usedWidth = 2 + name.len + 3 + desc.len
+ let statsVisual = visualLen(stats)
+ let padLen = 78 - usedWidth - statsVisual - 2
+ if padLen > 0:
+ stdout.write " ".repeat(padLen)
+ stdout.write stats
+ stdout.writeLine " " & BoxVertical
+ stdout.writeLine boxMid(78)
+ except CatchableError:
+ discard
+```
+
+**Test:**
+```bash
+just run --target /tmp | head -20
+```
+Verify module headers show "N findings (Xms)" right-aligned.
+
+---
+
+### Finding 4: Terminal box right-border alignment broken for variable content
+**Severity:** MAJOR
+**Axis:** Code Quality
+**Files:** src/output/terminal.nim:60-84, 98-126
+
+**Issue:** `renderFinding` writes descriptions and paths of arbitrary length
+then appends `" " & BoxVertical` with no padding to reach column 78. Long
+descriptions push past the box. Short ones leave the right border floating
+at different positions. Same issue in `renderSummary` — hardcoded
+`" ".repeat(69)` and `" ".repeat(20)` assume fixed content widths that
+vary with finding counts, module counts, and durations.
+
+**Proof:** A finding with path `/home/user/.config/google-chrome/Default/Login Data`
+(49 chars) plus permissions `[0644]` plus modified timestamp is ~90+ chars of
+content in a 78-char box. The right `BoxVertical` gets pushed to column ~95.
+A finding with path `/home/user/.pgpass` (18 chars) leaves the right border
+at ~column 50.
+
+**Proof Check:** Confidence: HIGH — the code has zero width calculation before
+writing the trailing BoxVertical.
+
+**Fix:**
+Create a `padWrite` helper that calculates visual width of content written so
+far and pads to fill the 78-char box before writing the closing border.
+Apply it to `renderFinding`, `renderSummary`, and `renderModuleErrors`.
+Truncate content that would exceed box width.
+
+In `src/output/terminal.nim`, add the `visualLen` proc from Finding 3
+(shared), then refactor each line that writes content + BoxVertical:
+```nim
+proc padToBox(content: string, boxWidth: int = 78) =
+ let vLen = visualLen(content)
+ let pad = boxWidth - vLen - 1
+ if pad > 0:
+ stdout.write " ".repeat(pad)
+ stdout.writeLine BoxVertical
+```
+
+Then each finding line becomes:
+```nim
+var line = BoxVertical & " " & sevBadge(f.severity) & " " & f.description
+stdout.write line
+padToBox(line)
+```
+
+Apply this pattern consistently to all content rows in the terminal renderer.
+
+**Test:**
+```bash
+just docker-test
+```
+Visual inspection of terminal output — all right borders should align at column 78.
+
+---
+
+### Finding 5: scanGitCredentials reports svHigh for empty credential files
+**Severity:** MAJOR
+**Axis:** Code Quality
+**Files:** src/collectors/git.nim:11-39
+
+**Issue:** If `.git-credentials` exists but is empty or contains no valid URLs,
+`credCount` stays at 0 but the function still creates a finding with
+"Plaintext Git credential store with 0 entries" at severity svHigh
+(or svCritical if world-readable). An empty file is not a high-severity
+credential exposure.
+
+**Proof:** Trace through `scanGitCredentials` with an empty `.git-credentials`:
+1. `safeFileExists` returns true
+2. `readFileLines` returns `@[]`
+3. Loop runs zero iterations, `credCount = 0`
+4. Code falls through to create credential and finding with `svHigh`
+5. Report shows "Plaintext Git credential store with 0 entries" as HIGH
+
+**Proof Check:** Confidence: HIGH — there is no guard checking `credCount > 0`
+before creating the finding.
+
+**Fix:**
+`src/collectors/git.nim` — add early return after counting:
+```nim
+if credCount == 0:
+ return
+```
+Insert after the for-loop that counts credentials (after line 22), before
+the credential/finding construction.
+
+**Test:**
+Create an empty `.git-credentials` file, run scanner, verify no git finding
+appears.
+
+---
+
+### Finding 6: `just test` references non-existent test_all.nim
+**Severity:** MAJOR
+**Axis:** Code Quality
+**Files:** Justfile:84-85
+
+**Issue:** The Justfile `test` recipe runs `nim c -r tests/test_all.nim`,
+but this file does not exist. There are no unit tests in the project.
+The only testing is Docker-based integration testing (validate.sh), which
+itself is broken (Finding 1).
+
+**Proof:** `test -f tests/test_all.nim` returns non-zero. The `tests/`
+directory contains only `docker/`.
+
+**Proof Check:** Confidence: HIGH — file does not exist.
+
+**Fix:**
+Create `tests/test_all.nim` with unit tests for each collector's core logic.
+At minimum, test:
+- `isPrivateKey` with various key headers
+- `isEncrypted` with encrypted/unencrypted markers
+- `matchesSecretPattern` with positive and negative cases
+- `matchesCommandPattern` (after fixing Finding 2) with all 7 patterns
+- `redactValue` edge cases
+- `permissionSeverity` logic
+- `parseModules` from CLI parsing
+
+These should be fast, in-process tests that don't require Docker or
+real credential files.
+
+**Test:**
+```bash
+just test
+```
+
+---
+
+### Finding 7: Missing credential categories — .netrc, npm/pip tokens, Terraform, Vault, GitHub CLI
+**Severity:** MAJOR
+**Axis:** Completeness
+**Files:** src/config.nim, src/collectors/apptoken.nim
+
+**Issue:** The tool covers 7 categories but misses several high-value
+credential stores that a post-access operator would check:
+
+| Missing Target | Path | Why It Matters |
+|---|---|---|
+| `.netrc` | `~/.netrc` | Universal HTTP auth store; Heroku, Artifactory, many tools |
+| `.npmrc` | `~/.npmrc` | npm registry auth tokens (`_authToken=`) |
+| `.pypirc` | `~/.pypirc` | PyPI upload tokens |
+| GitHub CLI | `~/.config/gh/hosts.yml` | GitHub OAuth tokens |
+| Terraform | `~/.terraform.d/credentials.tfrc.json` | Terraform Cloud API tokens |
+| Vault | `~/.vault-token` | HashiCorp Vault root/user tokens |
+| `~/.config/helm/repositories.yaml` | Helm chart repo credentials |
+| `~/.config/rclone/rclone.conf` | Cloud storage credentials (S3, GCS, etc.) |
+
+Industry comparison: LaZagne (closest post-access tool) covers 20+
+credential categories on Linux alone. `truffleHog` detects 700+ secret
+patterns. This tool's 7 categories leave real coverage gaps.
+
+**Proof:** `grep -r "netrc\|npmrc\|pypirc\|vault-token\|terraform\|gh/hosts" src/`
+returns zero matches.
+
+**Proof Check:** Confidence: HIGH — the files are either scanned or they're not.
+
+**Fix:**
+Add constants to `src/config.nim`:
+```nim
+const
+ NetrcFile* = ".netrc"
+ NpmrcFile* = ".npmrc"
+ PypircFile* = ".pypirc"
+ GhCliHosts* = ".config/gh/hosts.yml"
+ TerraformCreds* = ".terraform.d/credentials.tfrc.json"
+ VaultTokenFile* = ".vault-token"
+ HelmRepos* = ".config/helm/repositories.yaml"
+ RcloneConf* = ".config/rclone/rclone.conf"
+```
+
+Add scanning logic to `src/collectors/apptoken.nim` — each is a simple
+file-exists-and-check-contents pattern, consistent with existing
+`scanDbCredFiles` approach. `.netrc` deserves content parsing (look for
+`password` or `login` tokens). `.npmrc` should check for `_authToken=`.
+`.pypirc` should check for `password` under `[pypi]` section.
+
+**Test:**
+Add planted files to `tests/docker/planted/` and assertions to `validate.sh`.
+
+---
+
+### Finding 8: matchesExclude uses substring matching, not glob patterns
+**Severity:** MINOR
+**Axis:** Code Quality
+**Files:** src/collectors/base.nim:90-94
+
+**Issue:** `matchesExclude` checks `if pattern in path` — plain substring.
+An exclude pattern of `"env"` would exclude `/home/user/.venv/something`,
+`/home/user/environment/data`, and the intended `.env` file. The CLI help
+says `--exclude ` suggesting glob behavior, but the implementation
+is substring containment.
+
+**Proof:** `matchesExclude("/home/user/.venv/lib/site.py", @["env"])`
+returns `true`, excluding a Python virtualenv file that has nothing to do
+with environment secrets.
+
+**Proof Check:** Confidence: HIGH — `in` is Nim's substring containment
+operator for strings.
+
+**Fix:**
+`src/collectors/base.nim:90-94` — use `std/os.extractFilename` and simple
+glob matching, or at minimum document that patterns are substrings. Better
+fix: use Nim's `std/strutils.contains` with path-segment awareness:
+```nim
+proc matchesExclude*(path: string, patterns: seq[string]): bool =
+ let name = path.extractFilename()
+ for pattern in patterns:
+ if pattern in name or pattern in path.splitPath().head:
+ return true
+```
+
+Or implement basic glob support with `*` matching.
+
+**Test:**
+Unit test that `.venv/lib/site.py` is NOT excluded by pattern `".env"`.
+
+---
+
+### Finding 9: JSON renderJson silently discards file-write errors
+**Severity:** MINOR
+**Axis:** Code Quality
+**Files:** src/output/json.nim:72-85
+
+**Issue:** When `--output ` specifies an invalid path (read-only dir,
+nonexistent parent), `writeFile` throws, the exception is caught and
+discarded. The JSON is then also written to stdout, but if stdout is
+redirected and also fails, both errors are silently swallowed. The user
+gets zero indication that their requested output file was not created.
+
+**Proof:** Run `credenum --format json --output /root/nope.json` as
+non-root — the file write fails silently, output goes only to stdout.
+If stdout is piped to a broken pipe, both writes fail and the user
+sees nothing.
+
+**Proof Check:** Confidence: MEDIUM — the stdout fallback usually works,
+so the practical impact is limited to the file path case.
+
+**Fix:**
+`src/output/json.nim:77-80` — write a warning to stderr on file write failure:
+```nim
+except CatchableError as e:
+ try:
+ stderr.writeLine "Warning: could not write to " & outputPath & ": " & e.msg
+ except CatchableError:
+ discard
+```
+
+**Test:**
+```bash
+just run --format json --output /dev/full 2>&1 | grep "Warning"
+```
+
+---
+
+### Finding 10: redactLine strips leading quote but keeps trailing quote
+**Severity:** MINOR
+**Axis:** Code Quality
+**Files:** src/collectors/history.nim:15-28
+
+**Issue:** `redactLine` strips a leading `"` or `'` from the value via
+`value[1 .. ^1]`, but `^1` is the last index in Nim (inclusive), so
+this removes only the first character. Input `"secret"` becomes
+`secret"` — the trailing quote survives into the redacted preview.
+
+**Proof:** Input line `export API_KEY="mysecret"`:
+1. `eqIdx` = 14 (position of `=`)
+2. `value` = `"mysecret"` (after strip)
+3. `value.startsWith("\"")` → true
+4. `cleanValue` = `value[1 .. ^1]` = `mysecret"` (trailing quote kept)
+5. `redactValue("mysecret\"", 4)` = `myse****"`
+
+**Proof Check:** Confidence: HIGH — `^1` is the last character in Nim slice
+notation; this is deterministic.
+
+**Fix:**
+`src/collectors/history.nim:24-26`:
+```nim
+let cleanValue = if (value.startsWith("\"") and value.endsWith("\"")) or
+ (value.startsWith("'") and value.endsWith("'")):
+ value[1 ..< ^1]
+else:
+ value
+```
+
+Note: `^1` in `[1 ..< ^1]` excludes the last character (half-open range).
+
+**Test:**
+Unit test: `redactLine("export KEY=\"secret\"")` should produce `KEY=secr**`
+with no trailing quote.
+
+---
+
+### Finding 11: isRelative computed but unused in Firefox profile parsing
+**Severity:** MINOR
+**Axis:** Code Quality
+**Files:** src/collectors/browser.nim:11-48
+
+**Issue:** The `scanFirefox` proc parses `IsRelative=0` from profiles.ini
+and stores it in `isRelative`, but this variable is never read. Profile
+path resolution uses `profile.startsWith("/")` instead. The variable is
+dead code from an abandoned design path.
+
+**Proof:** `isRelative` is set on lines 23 and 37, but never appears in
+any conditional or expression after the parsing loop.
+
+**Proof Check:** Confidence: HIGH — grep for `isRelative` in browser.nim
+shows only assignments, zero reads.
+
+**Fix:**
+`src/collectors/browser.nim` — remove the `isRelative` variable entirely
+(lines 23, 37). The `startsWith("/")` check on line 43 is sufficient for
+Linux path detection.
+
+**Test:**
+```bash
+just check
+```
+Verify compilation succeeds with no warnings about unused variable.
+
+---
+
+### Finding 12: Azure scanner adds directory finding unconditionally
+**Severity:** MINOR
+**Axis:** Code Quality
+**Files:** src/collectors/cloud.nim:140-144
+
+**Issue:** `scanAzure` always adds an svInfo finding for the Azure CLI
+directory after checking for specific token files. If token cache findings
+were already added, this creates redundant noise. If no tokens were found,
+a bare directory finding at svInfo adds very little value.
+
+**Proof:** If `~/.azure/` exists with `accessTokens.json`, the output shows:
+1. "Azure token cache" at svMedium — useful
+2. "Azure CLI configuration directory" at svInfo — noise, adds nothing
+
+**Proof Check:** Confidence: MEDIUM — it's noise, not incorrect data. Could
+argue the directory finding is useful as a "this user has Azure CLI installed"
+signal, but only if no token files were found.
+
+**Fix:**
+`src/collectors/cloud.nim:140-144` — only add the directory finding if no
+token files were found:
+```nim
+if result.findings.len == 0 or
+ result.findings[^1].category != catCloud:
+ result.findings.add(makeFinding(
+ azDir,
+ "Azure CLI configuration directory",
+ catCloud, svInfo
+ ))
+```
+
+Better: track whether any Azure-specific findings were added and only emit
+the directory finding as a fallback.
+
+**Test:**
+Docker test — verify Azure directory finding only appears when no token
+findings exist.
+
+---
+
+## Self-Interrogation
+
+Looking at these 12 findings as a whole:
+
+- **Did I miss a dimension?** The tool has no rate-limiting or size-limiting on
+ file reads. `readFileContent` reads entire files into memory. A malicious
+ (or just large) `.bash_history` of several GB would cause OOM. But the
+ history scanner has `MaxHistoryLines = 50000` via `readFileLines`, which
+ mitigates this for its use case. Other collectors reading full files
+ (git config, kubeconfig) are typically small. Not worth a finding.
+
+- **Are any findings weak?** Finding 12 (Azure directory) is the weakest —
+ it's a UX preference, not a bug. Keeping it as MINOR is appropriate.
+ Finding 11 (dead variable) is real but trivial. Everything MAJOR and above
+ is solid.
+
+- **Completeness check:** The tool has 7 modules covering the major
+ categories but Finding 7 lists 8 specific credential stores that any
+ practitioner would expect. The `.netrc` omission alone is notable since
+ it's been the standard Unix credential store since the 1980s.
+
+## Summary
+
+**Total Findings:** 12 (2 critical, 5 major, 5 minor)
+**Code Quality Findings:** 11
+**Completeness Findings:** 1
diff --git a/docs/superpowers/specs/2026-04-01-credential-enumeration-design.md b/docs/superpowers/specs/2026-04-01-credential-enumeration-design.md
new file mode 100644
index 00000000..959bca97
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-01-credential-enumeration-design.md
@@ -0,0 +1,257 @@
+# Credential Enumeration Tool — Design Spec
+
+## Overview
+
+A post-access credential enumeration tool written in Nim that scans Linux systems for exposed secrets across 7 categories. Compiles to a single static binary with zero dependencies — drop on target, run, get a structured report of every credential file, its exposure level, and severity rating.
+
+**Language:** Nim 2.2.x
+**Binary name:** `credenum`
+**Architecture:** Modular collector pattern — one module per credential category, common interface, central runner
+
+---
+
+## Core Types (`src/types.nim`)
+
+- **Severity** — enum: `info`, `low`, `medium`, `high`, `critical`
+- **Category** — enum: `browser`, `ssh`, `cloud`, `history`, `keyring`, `git`, `apptoken`
+- **Credential** — discovered credential data (source, credential type, value or redacted preview, metadata)
+- **Finding** — a single discovery (path, category, severity, description, optional Credential, file permissions, timestamps)
+- **CollectorResult** — `seq[Finding]` + collector metadata (name, duration, errors encountered)
+- **HarvestConfig** — runtime configuration (target home dir, enabled modules, exclude patterns, output format, flags)
+- **Report** — all collector results + summary stats + timestamp + target info
+
+**Severity assignment rules:**
+- Critical: plaintext credentials in world-readable files
+- High: unprotected private keys, plaintext credential stores
+- Medium: overly permissive file permissions on credential files
+- Low: credential files exist but properly permissioned
+- Info: enumeration data (host lists, profile counts, existence checks)
+
+---
+
+## Collector Modules
+
+Each module exports `proc collect(config: HarvestConfig): CollectorResult`. The runner calls each in sequence. No inheritance needed — just a common return type and a seq of collector procs populated at init.
+
+### 1. Browser Credential Store Scanner (`src/collectors/browser.nim`)
+- Firefox: locate profiles via `profiles.ini`, check `logins.json`, `cookies.sqlite`, `key4.db`
+- Chromium: locate `Login Data`, `Cookies`, `Web Data` SQLite databases
+- Report: file locations, permissions, entry counts, last-modified timestamps
+- Flag world-readable/group-readable databases as critical
+- Detection + metadata level (no decryption)
+
+### 2. SSH Key & Config Auditor (`src/collectors/ssh.nim`)
+- Scan `~/.ssh/` for private keys (RSA, Ed25519, ECDSA, non-standard filenames)
+- Read key headers to determine passphrase protection (encrypted PEM vs unencrypted)
+- Flag unprotected keys as high severity
+- Check permissions (keys=600, directory=700)
+- Parse `~/.ssh/config` — enumerate hosts, identify weak settings
+- Read `authorized_keys` and `known_hosts` for enumeration
+
+### 3. Cloud Provider Config Scanner (`src/collectors/cloud.nim`)
+- AWS: `~/.aws/credentials`, `~/.aws/config` — count profiles, identify static vs session keys
+- GCP: `~/.config/gcloud/` — application default credentials, service account keys
+- Azure: `~/.azure/` — access tokens, profile info
+- Kubernetes: `~/.kube/config` — enumerate contexts, clusters, auth methods
+- Permission checks, flag anything broader than owner-only
+
+### 4. Shell History & Environment Scanner (`src/collectors/history.nim`)
+- Read `.bash_history`, `.zsh_history`, `.fish_history`
+- Pattern match for inline secrets: KEY=, SECRET=, TOKEN=, PASSWORD= exports, DB connection strings, curl/wget with auth headers
+- Scan for `.env` files in home directory tree
+- Report: file, line region, redacted preview
+
+### 5. Keyring & Password Store Scanner (`src/collectors/keyring.nim`)
+- GNOME Keyring: `~/.local/share/keyrings/`
+- KDE Wallet: `~/.local/share/kwalletd/`
+- KeePass/KeePassXC: search for `.kdbx` files
+- pass (password-store): `~/.password-store/`
+- Bitwarden: `~/.config/Bitwarden/` local vault data
+- Report locations, file sizes, permissions, last modified
+
+### 6. Git Credential Store Scanner (`src/collectors/git.nim`)
+- `~/.git-credentials` — plaintext storage (high severity)
+- `~/.gitconfig` — check `credential.helper` setting
+- Search for credential cache socket files
+- Check for GitHub/GitLab PATs in config files
+
+### 7. Application Token Scanner (`src/collectors/apptoken.nim`)
+- Slack: `~/.config/Slack/` session/cookie storage
+- Discord: `~/.config/discord/` token storage
+- VS Code: `~/.config/Code/` stored secrets
+- Database configs: `~/.pgpass`, `~/.my.cnf`, Redis configs
+- MQTT broker configs, common application credential files
+
+---
+
+## CLI Interface
+
+```
+credenum [flags]
+
+Flags:
+ --target Target user home directory (default: current user)
+ --modules Comma-separated module list (default: all)
+ --exclude Glob patterns for paths to skip
+ --format Output format: terminal, json, both (default: terminal)
+ --output Write JSON output to file
+ --dry-run List paths that would be scanned without reading
+ --quiet Suppress banner and progress, output findings only
+ --verbose Show all scanned paths, not just findings
+```
+
+**CLI parsing:** `std/parseopt` (stdlib, no dependencies)
+
+---
+
+## Terminal Output Design
+
+Hacker-aesthetic terminal output:
+- ASCII art banner with tool name and version
+- Box-drawing characters for section borders
+- Color-coded severity badges (critical=red, high=magenta, medium=yellow, low=cyan, info=dim)
+- Clean table formatting for findings
+- Summary footer with totals by severity, modules scanned, duration
+- Progress indicators showing which module is currently scanning
+
+---
+
+## Output Formats
+
+### Terminal (ANSI)
+Colored, formatted output designed for interactive use. Banner, per-module sections, severity badges, summary.
+
+### JSON
+Structured report:
+```json
+{
+ "metadata": { "timestamp": "...", "target": "...", "version": "...", "duration_ms": 0 },
+ "modules": [
+ {
+ "name": "ssh",
+ "findings": [
+ {
+ "category": "ssh",
+ "severity": "high",
+ "path": "/home/user/.ssh/id_rsa",
+ "description": "Unprotected private key (no passphrase)",
+ "permissions": "0644",
+ "modified": "2026-01-15T10:30:00Z"
+ }
+ ],
+ "duration_ms": 12,
+ "errors": []
+ }
+ ],
+ "summary": { "critical": 2, "high": 5, "medium": 8, "low": 3, "info": 12 }
+}
+```
+
+---
+
+## Build & Distribution
+
+### Static binary via musl
+- `config.nims` configures musl-gcc for fully static Linux binaries
+- Zero runtime dependencies
+
+### Cross-compilation
+- x86_64-linux (primary)
+- aarch64-linux (ARM64)
+- Uses zig cc for cross-compilation
+- Justfile tasks: `just build-x86`, `just build-arm64`
+
+### Build modes
+- `just build` — debug build with all checks
+- `just release` — optimized static binary (`-d:release -d:lto --opt:size`)
+- `just release-small` — stripped + UPX compressed
+
+### Justfile tasks
+- `just build` / `just release` / `just release-small`
+- `just test` — run unit tests
+- `just docker-test` — build + run in Docker test environment
+- `just fmt` — format with nph
+- `just clean`
+
+---
+
+## Docker Test Environment
+
+**`tests/docker/Dockerfile`** — Ubuntu-based container planting fake credentials across all 7 categories:
+
+- SSH: test key pairs (some protected, some not), various permissions
+- Browser: mock Firefox profile with dummy `logins.json`, mock Chromium dirs
+- Cloud: fake AWS credentials, dummy GCP service account JSON, mock kubeconfig
+- History: seeded `.bash_history`/`.zsh_history` with fake tokens
+- Keyrings: mock `.kdbx`, mock `pass` store
+- Git: `.git-credentials` with dummy entries
+- App tokens: mock Slack/Discord/VS Code configs, `.pgpass`, `.my.cnf`
+
+All values are obviously fake (`AKIA_FAKE_ACCESS_KEY_12345`).
+
+`just docker-test` builds, runs credenum inside, validates all findings discovered with correct severity.
+
+---
+
+## Project Structure
+
+```
+credential-enumeration/
+├── src/
+│ ├── harvester.nim # Entry point, CLI parsing
+│ ├── config.nim # Constants, paths, patterns, severities
+│ ├── types.nim # Core types
+│ ├── runner.nim # Execute collectors, aggregate results
+│ ├── output/
+│ │ ├── terminal.nim # ANSI terminal output with hacker aesthetic
+│ │ └── json.nim # JSON serialization
+│ └── collectors/
+│ ├── base.nim # Collector registration
+│ ├── browser.nim
+│ ├── ssh.nim
+│ ├── cloud.nim
+│ ├── history.nim
+│ ├── keyring.nim
+│ ├── git.nim
+│ └── apptoken.nim
+├── tests/
+│ └── docker/
+│ ├── Dockerfile
+│ └── planted/ # Mock credential files
+├── learn/
+│ ├── 00-OVERVIEW.md
+│ ├── 01-CONCEPTS.md
+│ ├── 02-ARCHITECTURE.md
+│ ├── 03-IMPLEMENTATION.md
+│ └── 04-CHALLENGES.md
+├── config.nims # Build config (static linking, cross-compile)
+├── credential-enumeration.nimble # Package manifest
+├── Justfile
+├── install.sh
+├── README.md
+├── LICENSE
+└── .gitignore
+```
+
+---
+
+## Learn Folder
+
+- **00-OVERVIEW.md** — What credential enumeration is, why it matters, prerequisites, quick start
+- **01-CONCEPTS.md** — Linux credential storage locations, file permission model, where apps store secrets and why defaults are insecure. Real-world breach references.
+- **02-ARCHITECTURE.md** — Modular collector design, data flow, why Nim for security tooling
+- **03-IMPLEMENTATION.md** — Code walkthrough: core types, collector pattern, CLI parsing, output formatting, Nim type system and modules
+- **04-CHALLENGES.md** — Extensions: new collectors, encrypted output, network enumeration, framework integration
+
+---
+
+## What This Project Teaches
+
+- Linux credential storage locations across browsers, SSH, cloud tools, shells, keyrings, Git, and applications
+- File permission models and their security implications
+- Nim programming: static compilation, module system, type system, FFI potential
+- Why Nim is adopted in the security assessment community (small static binaries, C-level performance)
+- Modular tool architecture with common interfaces
+- Building visually polished CLI tools
+- Docker-based testing for security tools
+- Cross-compilation and static linking for portable binaries