Skip to content

feat: surface .npmrc and pip configuration on the host#69

Open
swarit-stepsecurity wants to merge 6 commits intostep-security:mainfrom
swarit-stepsecurity:swarit/feat/wt/rc-info-npm-pip
Open

feat: surface .npmrc and pip configuration on the host#69
swarit-stepsecurity wants to merge 6 commits intostep-security:mainfrom
swarit-stepsecurity:swarit/feat/wt/rc-info-npm-pip

Conversation

@swarit-stepsecurity
Copy link
Copy Markdown
Member

Summary

Adds two read-only audits that enumerate package-manager configuration on the host. Surface-only — no drift detection, no auto-remediation, no traffic, no plaintext credentials in any output.

Two commits, intentionally split for review:

  • feat(npmrc) (41e17d7) — every .npmrc across all four npm scopes (built-in / global / user / project), parsed (with auth values redacted to ***last4 and ${VAR} env-refs preserved verbatim), plus the merged effective view npm itself would resolve via npm config ls -l --json with per-key source attribution.
  • feat(pipconfig) (18e6959) — every pip.conf / pip.ini across all four pip scopes (global / user / user-legacy / site / PIP_CONFIG_FILE override), parsed, plus the merged effective view from pip config list -v with per-key source attribution. Adds a fixed catalog of finding IDs (pip-001 .. pip-024) covering embedded creds, http:// schemes, extra-index-url (dependency-confusion), trusted-host, no-build-isolation, file-mode escalations, legacy paths, and PIP_CONFIG_FILE redirection.

Both audits run as part of the standard scan and surface in:

  • JSON / enterprise telemetry — top-level npmrc_audit / pip_audit fields on ScanResult and Payload
  • --pretty — compact summary block for each
  • --npmrc — verbose pretty view of just the npm audit (also --npmrc --json)
  • --pipconfig — verbose pretty view of just the pip audit, findings first (also --pipconfig --json)

What's intentionally out of scope (documented for follow-up)

The npmrc audit is inventory-only. Drift / change tracking (snapshot to disk, diff next run, attribute writers) and per-project effective overrides ("if I cd into this cloned repo and run npm install, what flips?") are deliberately deferred. The model already carries SHA256 and ValueSHA256 fingerprints so a future drift layer can land cheaply on top of what's here. See `.plans/0005-npmrc-audit.md` (worktree-local) for extension notes.

Test plan

  • go test ./... — full suite green on darwin (no cache, fresh run)
  • Cross-compile clean for linux/amd64 and windows/amd64
  • Commit 1 alone (npm only) builds + tests cleanly — bisect-safe
  • Manual --npmrc smoke run on macOS — discovers global / user / project files, redaction works on real auth tokens, ${VAR} env-refs preserved
  • Manual --pipconfig smoke run on macOS — finds all 5 pip scope candidates, surfaces ~/.netrc permissions finding
  • End-to-end validated on the Fedora EC2 test machine in earlier iterations: pip findings (pip-001 CRITICAL embedded creds, pip-005 HIGH extra-index-url, pip-007 HIGH trusted-host, pip-022 HIGH file-perms-with-creds, pip-011 MEDIUM no-build-isolation, pip-020 MEDIUM PIP_CONFIG_FILE redirect, pip-023/pip-024 INFO defensive controls) all fire as expected against seeded fixtures across /etc/pip.conf, ~/.config/pip/pip.conf, ~/test-venv/pip.conf, and PIP_CONFIG_FILE=/tmp/...
  • Re-run on Linux EC2 against this branch (PR is functionally identical to validated state but worth re-confirming)

Adds a read-only audit that enumerates each .npmrc across all four npm
config scopes (built-in, global, user, project), parses every key/value
(redacting auth values to ***last4, preserving ${VAR} env-refs), and
captures the merged effective view npm itself would resolve via
`npm config ls -l --json` plus source attribution from `npm config ls -l`.

Surfaces in:
- standard scan: compact summary section in --pretty + JSON via
  ScanResult.NPMRCAudit / Payload.NPMRCAudit
- new --npmrc flag: focused, verbose pretty view (or JSON via
  --npmrc --json) for deep inspection without running the rest of the
  scan; ~1s runtime
- HTML / enterprise telemetry payloads pick up the audit object
  automatically through the model wiring

Drift detection (snapshot/diff across runs), per-project effective
overrides, and severity scoring are intentionally out of scope here.
The model carries SHA256 / ValueSHA256 fingerprints today specifically
so a future drift layer can land cheaply on top of the surface
inventory. See .plans/0005-npmrc-audit.md for extension notes.
…ation

Adds a read-only audit that mirrors the npmrc one but covers pip:
discovery via `pip config debug` (with OS-specific path-enumeration
fallback when pip isn't installed), parsing of every pip.conf / pip.ini
across all four scopes (global / user / user-legacy / site / PIP_CONFIG_FILE),
the merged effective view from `pip config list -v` with per-key source
attribution, a snapshot of PIP_* environment variables, and a
`~/.netrc` presence/permissions probe.

Where the pip audit goes further than npmrc: it ships a fixed catalog
of finding IDs (pip-001 .. pip-024) per the spec — embedded creds in
URLs, http:// schemes, extra-index-url presence (dependency-confusion),
trusted-host, no-build-isolation, file mode escalations, legacy paths,
PIP_CONFIG_FILE redirection, etc. Each finding carries severity,
category, source attribution, redacted value, detail, and a remediation
hint. Auth tokens and proxy credentials are always rendered as
`user:****@host` and the raw value never leaves the detector.

Surfaces in:
- standard scan: compact summary section in --pretty (severity bucket
  counts) plus the full audit object in JSON / enterprise telemetry
- new --pipconfig flag: focused, verbose pretty view (or JSON via
  --pipconfig --json) — findings first, then files, then effective
  view, then env vars + ~/.netrc

End-to-end validated on Fedora EC2 with seeded fixtures across all
four scopes (PIP_CONFIG_FILE redirect, http:// scheme on index-url,
extra-index-url with embedded creds, no-build-isolation, etc.).
See .plans/0006-pip-config-audit.md for the full design.
…figaudit

Moves the 15 npmrc + pipconfig files out of internal/detector/ and into
a dedicated sub-package internal/detector/configaudit/. Pure rename + a
package-decl change in each file (`package detector` → `package configaudit`)
plus updated imports in the three callsites:

- internal/scan/scanner.go
- internal/telemetry/telemetry.go
- cmd/stepsecurity-dev-machine-guard/main.go

`detector.NewNPMRCDetector(...)` becomes `configaudit.NewNPMRCDetector(...)`
and likewise for `NewPipConfigDetector`. No public-API changes beyond the
package qualifier.

Why: internal/detector/ had grown to 55 files and the rc/config-file
audits are a self-contained subset (15 files, no cross-references with
sibling detector files). Splitting them off drops detector/ to 40 files
and clarifies the architecture: detector/ owns inventory-style detectors
(IDEs, AI tools, package managers, etc.); configaudit/ owns config-file
audits with their own discovery + parsing + finding semantics. Future
sibling audits — .yarnrc.yml, .gemrc, .cargo/config.toml — fit naturally
under configaudit/.

Verified: full test suite green (configaudit tests run as a separate
package now), cross-compile clean for darwin/linux/windows.
…ffix

Two bugs surfaced by end-to-end testing on Fedora EC2 with a 69-scenario
harness:

1. Plaintext credential leak in `effective.config`. `pip config list -v`
   emits URL values verbatim, including any embedded `user:pass@host`
   userinfo. We were copying the value into `effective.config[<key>]`
   without redaction — so a hardcoded token in `extra-index-url` would
   show up in the JSON output even though the per-file `entries` view
   already redacted it. Fix: run `redactCredsInValue` over the value
   before storing in the merged map.

2. `pip-019` (legacy `~/.pip/pip.conf`) only fired when the discovery
   layer was tagged `user-legacy`. But when `pip` is installed, our
   discovery uses `pip config debug`, which reports the legacy path
   under the `user` layer (pip itself doesn't expose the "legacy"
   concept) — so the rule silently never fired in practice. Fix:
   detect by path suffix (`isLegacyPipConfigPath`) instead of the
   layer label.

   The Windows discriminator carefully excludes both `%APPDATA%\...`
   (current user) and `%PROGRAMDATA%\...` (global) so only the
   `%HOME%\pip\pip.ini` form trips the rule.

Also: `PipKeyValue.Values` is now `json:"-"`. It holds the raw,
un-redacted parsed values for the findings engine to inspect (URL.User
parsing, http-scheme detection, etc.) — it must never be serialized,
because for keys like `extra-index-url` it can hold a literal
`user:pass@host` URL. The `Display` field is the canonical user-facing
rendering and it stays in JSON.

Three new unit tests lock all of this in:
- `TestParseEffective_RedactsEmbeddedCreds`
- `TestIsLegacyPipConfigPath` (10 cases incl. all three Windows variants)
- `TestEvaluateFileLevel_Pip019_FiresOnLegacyPathRegardlessOfLayer`

Validated on Fedora EC2: 69/69 scenarios pass.
Reusable shell harness covering ~65 scenarios for the npmrc + pip
config audits — discovery across all four scopes for each tool,
credential redaction, env-var interactions (NPM_CONFIG_USERCONFIG,
PIP_CONFIG_FILE incl. /dev/null disable, VIRTUAL_ENV), every
pip-001..pip-024 finding rule, file-mode escalations, severity
ordering, and edge cases (missing files, unreadable files, garbage
content, symlinks).

Usage:
  tests/test_rc_audit.sh                          # uses ./stepsecurity-dev-machine-guard
  BINARY=/path/to/binary tests/test_rc_audit.sh   # explicit binary

Conventions match tests/test_smoke_go.sh (pass/fail/section helpers,
PASS/FAIL/SKIP counts, color-coded output, exits non-zero on any FAIL).

Safety: backs up any pre-existing user config (~/.npmrc, ~/.netrc,
~/.config/pip/pip.conf, ~/.pip/pip.conf) on entry and restores via
trap on EXIT/INT/TERM, so a developer running this against their own
machine never loses config. Sudo-required scenarios (writing
/etc/pip.conf for the global-scope test) are skipped automatically
when passwordless sudo is unavailable; git scenarios skip if git is
absent. No secrets, no hardcoded paths to credential stores, no
remote-machine assumptions.

Verified on:
  - macOS (BINARY=/tmp/dmg-rc-mac):    PASS=64  FAIL=0  SKIP=1 (no sudo)
  - Fedora 42 EC2 (BINARY=~/dmg-rc):    PASS=65  FAIL=0  SKIP=0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant