From 4ec156ecf80bf7cc74bc4bf6ea19e8ef5a9faf16 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Wed, 8 Apr 2026 23:56:03 -0400 Subject: [PATCH] delete --- ...4-01-credential-enumeration-audit-fixes.md | 591 ------------------ ...026-04-01-credential-enumeration-design.md | 257 -------- 2 files changed, 848 deletions(-) delete mode 100644 docs/plans/2026-04-01-credential-enumeration-audit-fixes.md delete mode 100644 docs/superpowers/specs/2026-04-01-credential-enumeration-design.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 deleted file mode 100644 index e4545b73..00000000 --- a/docs/plans/2026-04-01-credential-enumeration-audit-fixes.md +++ /dev/null @@ -1,591 +0,0 @@ -# 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 deleted file mode 100644 index 959bca97..00000000 --- a/docs/superpowers/specs/2026-04-01-credential-enumeration-design.md +++ /dev/null @@ -1,257 +0,0 @@ -# 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