@kushankurdas/npm-sentinel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ # Command: `baseline`
2
+
3
+ **Syntax:**
4
+
5
+ ```bash
6
+ npm-sentinel baseline save [flags]
7
+ npm-sentinel baseline diff [flags]
8
+ ```
9
+
10
+ ## What “baseline” means here
11
+
12
+ A **saved snapshot** of your **trusted** dependency picture for **watched** packages only:
13
+
14
+ - **Watched packages** = names from root `package.json` **`dependencies`**, **`devDependencies`**, **`optionalDependencies`**, **`peerDependencies`**, plus **`watchPackagesExtra`**, or **`watchPackagesOverride`** if set (`lib/config.js`).
15
+ - For each watched name that **resolves** in the lockfile, the snapshot stores:
16
+ - **Resolved version**
17
+ - **Direct dependency names** of that package in the tree (children in lockfile)
18
+ - **Lifecycle script keys** from the registry **packument** for that name at that version (`preinstall`, `install`, `postinstall`, etc., summarized in `lib/packument.js`)
19
+
20
+ Default file: **`.npm-sentinel-baseline.json`** (override with `--baseline-file`).
21
+
22
+ ## `baseline save`
23
+
24
+ ### Behavior
25
+
26
+ 1. Requires **`package-lock.json`** — otherwise exits **2**.
27
+ 2. Loads `package.json`, config, lockfile; computes **`watchNames`**.
28
+ 3. For each watched package, **`buildBaselineSnapshot`** (`lib/baseline.js`) resolves the package in the lockfile and **fetches** version metadata from the registry (needs **network**).
29
+ 4. Writes JSON snapshot; prints path and count of watched packages (or JSON with `ok`, `path`, `snapshot` if `--json`).
30
+
31
+ ### Exit codes
32
+
33
+ | Code | Meaning |
34
+ |------|---------|
35
+ | **0** | Snapshot written. |
36
+ | **2** | Missing lockfile or other failure. |
37
+
38
+ ### How to test
39
+
40
+ ```bash
41
+ cd /path/to/your-app # has package.json + package-lock.json
42
+ npx npm-sentinel baseline save
43
+ git add .npm-sentinel-baseline.json
44
+ ```
45
+
46
+ Verify the file exists and lists expected packages under **`packages`**.
47
+
48
+ Use **`--baseline-file /tmp/test-baseline.json`** in throwaway dirs.
49
+
50
+ ---
51
+
52
+ ## `baseline diff`
53
+
54
+ ### Behavior
55
+
56
+ 1. Loads baseline (default path). If **missing**, prints hint and exits **2**.
57
+ 2. Requires **`package-lock.json`** — otherwise exits **2**.
58
+ 3. Recomputes **`watchNames`** from **current** `package.json` + config.
59
+ 4. **`diffAgainstBaseline`** (`lib/diff-signals.js`): for each watched package that existed in the saved baseline, compares saved state vs **current** lockfile + **current** registry scripts.
60
+
61
+ ### Signal types
62
+
63
+ | `type` | Typical `severity` | Meaning |
64
+ |--------|-------------------|---------|
65
+ | `missing_resolution` | **error** | Watched package no longer resolves under `node_modules` in the lockfile. |
66
+ | `version_change` | **warn** | Resolved version changed vs baseline. |
67
+ | `new_dependency` | **error** | New **direct** child dependency on the watched package (possible injection). |
68
+ | `dependency_removed` | **warn** | A direct child was removed (account takeover / metadata oddity — can be noisy). |
69
+ | `new_lifecycle_script` | **error** | Registry scripts for this name@version include a lifecycle key not present in baseline. |
70
+
71
+ Messages and `detail` objects are set in **`lib/diff-signals.js`**.
72
+
73
+ ### Output
74
+
75
+ - Human: one line per signal, prefixed with `[error]` or `[warn]`, or **“No baseline drift detected.”**
76
+ - **`--json`:** `{ "ok": <no error signals>, "signals": [...] }`
77
+
78
+ ### Exit codes
79
+
80
+ | Code | Meaning |
81
+ |------|---------|
82
+ | **0** | No signals **or** only **`warn`** signals. |
83
+ | **1** | At least one **`error`** signal. |
84
+ | **2** | No baseline file, missing lockfile, etc. |
85
+
86
+ **Note:** **`warn` alone does not fail** `baseline diff`.
87
+
88
+ ### How to test
89
+
90
+ 1. `baseline save` on a known-good tree.
91
+ 2. **Version bump:** `npm install lodash@<other-version>` (if lodash is a root dep), then `baseline diff` → expect **`version_change`** (warn).
92
+ 3. **New transitive shape:** Upgrade a root dep to a release that adds a **new direct** child on that package (e.g. supply-chain style) → **`new_dependency`** (error).
93
+ 4. **Clean run:** Immediately `diff` after `save` with no edits → **no drift**, exit **0**.
94
+
95
+ Combining with **`check`:** use **`check --baseline`** so vulnerability findings and baseline errors fail one command together ([check.md](check.md)).
96
+
97
+ ## Flags
98
+
99
+ `--cwd`, `--json`, `--baseline-file` — see [../reference/flags.md](../reference/flags.md).
100
+
101
+ ## Requirements
102
+
103
+ - **`package-lock.json`** (lockfile v1/v2/v3 supported by `lib/parse-npm-lockfile.js`).
104
+ - **Network** for `save` and for `diff` when comparing **lifecycle scripts** (packument fetch for current versions).
@@ -0,0 +1,92 @@
1
+ # Command: `check`
2
+
3
+ **Syntax:** `npm-sentinel check [flags]`
4
+ **Default:** If you run `npm-sentinel` with no subcommand, it runs **`check`** (`bin/cli.js`).
5
+
6
+ ## What it does
7
+
8
+ Runs the **static** pipeline (no `npm install` / `npm ci` on your machine):
9
+
10
+ 1. **Reads** `package.json` and **`package-lock.json`** in the project root (`--cwd`). Missing either file throws and exits **2**.
11
+ 2. **Parses** the lockfile and collects every unique **`(package name, resolved version)`** pair.
12
+ 3. **Findings (vulnerability / IOC):**
13
+ - **OSV** — unless `--offline` or `--no-osv`, POSTs batches to `https://api.osv.dev/v1/querybatch` (npm ecosystem). Each row with matching vulns becomes a finding (`source: osv`).
14
+ - **Offline IOCs** — **always** evaluated from `lib/offline-iocs.json`: exact name + version matches (`source: offline-ioc`, severity **critical** for filtering purposes).
15
+ - **`--min-severity`** filters findings (low → critical scale in `lib/merge-findings.js`).
16
+ 4. **Baseline (optional):** With **`--baseline`**, if **`.npm-sentinel-baseline.json`** exists under **`--cwd`**, runs **`diffAgainstBaseline`** and appends **signals**. Only the default baseline filename is supported on this code path (see [baseline.md](baseline.md), [gate.md](gate.md)).
17
+ 5. **`--npm-audit`:** Spawns **`npm audit --json`** in the project. Results are attached to the output as **`npmAuditFindings`** for display/JSON.
18
+
19
+ ## Output
20
+
21
+ - **Human:** Count of scanned pairs, list of findings (name, version, source, severity, ids, optional summary), baseline signals, short npm-audit summary (first ~20 entries).
22
+ - **`--json`:** Single object including `ok` (**true** only when no findings **and** no baseline **error**-severity signals), `findings`, `signals`, `npmAuditFindings`, `watchNames`, `packagesScanned`, `cwd`.
23
+
24
+ ## Exit codes
25
+
26
+ | Code | Meaning |
27
+ |------|---------|
28
+ | **0** | No findings after severity filter **and** no baseline signals with **`severity === "error"`**. |
29
+ | **1** | At least one finding **or** at least one **error** baseline signal. |
30
+ | **2** | Missing `package.json` / `package-lock.json`, OSV/network failure, or other thrown error. |
31
+
32
+ **Note:** **`npm audit` problems do not set exit code 1** by themselves. Only **`findings`** (OSV/IOC) and **baseline error signals** affect success for `check`.
33
+
34
+ ## Flags (summary)
35
+
36
+ See [../reference/flags.md](../reference/flags.md). Common: `--cwd`, `--json`, `--min-severity`, `--no-osv`, `--offline`, `--baseline`, `--npm-audit`.
37
+
38
+ ## How to test
39
+
40
+ ### Smoke: help and happy path
41
+
42
+ ```bash
43
+ npm-sentinel check --cwd /path/to/project
44
+ ```
45
+
46
+ Use a project with a clean lockfile and no baseline errors — expect exit **0** (or findings if deps are vulnerable).
47
+
48
+ ### IOC / offline path (no OSV)
49
+
50
+ Use a fixture lockfile that pins a version listed in **`lib/offline-iocs.json`** (e.g. `axios@1.14.1`). This repo’s test uses **`test/fixtures/lock-v3-mini.json`** copied into a temp dir:
51
+
52
+ ```bash
53
+ node bin/cli.js check --cwd /tmp/test-proj --offline --no-osv
54
+ ```
55
+
56
+ Expect exit **1** and output mentioning the IOC package.
57
+
58
+ ### OSV path
59
+
60
+ Install an old version with a known advisory, ensure **`package-lock.json`** pins it, run **`check`** without `--offline`. Expect findings if OSV has the CVE.
61
+
62
+ ### Severity filter
63
+
64
+ ```bash
65
+ npm-sentinel check --min-severity high
66
+ ```
67
+
68
+ Low/moderate-only findings are dropped; exit **0** if nothing meets the threshold.
69
+
70
+ ### Baseline integration
71
+
72
+ 1. `npm-sentinel baseline save`
73
+ 2. Change a watched dependency’s resolved version in the lockfile.
74
+ 3. `npm-sentinel check --baseline`
75
+
76
+ Expect **baseline signals** in output; exit **1** if any signal is **`error`** (e.g. `new_dependency`, `new_lifecycle_script`).
77
+
78
+ ### npm audit attachment
79
+
80
+ ```bash
81
+ npm-sentinel check --npm-audit --json
82
+ ```
83
+
84
+ Inspect **`npmAuditFindings`** in JSON; exit code still follows findings/baseline rules only.
85
+
86
+ ## Typical CI usage
87
+
88
+ ```bash
89
+ npx npm-sentinel@latest check --baseline
90
+ ```
91
+
92
+ Pair with a committed **`.npm-sentinel-baseline.json`** after an intentional `baseline save`.
@@ -0,0 +1,70 @@
1
+ # Command: `gate`
2
+
3
+ **Syntax:** `npm-sentinel gate [flags]`
4
+
5
+ ## What it does
6
+
7
+ A **CI-oriented** orchestration of **static check** and **optional sandbox**:
8
+
9
+ 1. Runs **`runCheck`** with **`withBaselineDiff: true`** always. If **`.npm-sentinel-baseline.json`** exists in the project root, baseline diff runs the same way as **`check --baseline`**.
10
+
11
+ **Baseline path limitation:** `runCheck` calls **`loadBaseline(cwd)`** with no custom path (`lib/scan.js`). Only the default filename **`.npm-sentinel-baseline.json`** is loaded for **`check --baseline`** and **`gate`**. The **`--baseline-file`** flag applies to **`baseline save`** / **`baseline diff`** only. For a custom path with static check, the CLI would need a small extension.
12
+
13
+ 2. **Failure (stage `check`):** If **`findings.length > 0`** **or** any baseline signal with **`severity === "error"`**, prints result (human or JSON with `gate: "failed", stage: "check"`), exits **1**.
14
+
15
+ 3. **Success + `--require-sandbox`:** Calls **`cmdSandbox`** with the same `flags` **but forces `json: false`** for the sandbox step — sandbox output is always human-readable even if you passed `--json` to `gate`.
16
+
17
+ 4. **Success without sandbox:** Prints **“Gate OK …”** or minimal JSON with `gate: "ok"`, exits **0**.
18
+
19
+ ## Flags
20
+
21
+ Inherits **check-related** flags: `--cwd`, `--json`, `--min-severity`, `--no-osv`, `--offline`, `--npm-audit`, plus **`--require-sandbox`**.
22
+
23
+ Sandbox flags **`--no-build`**, **`--mount-ssh`**, **`--ssh-dir`** apply when **`--require-sandbox`** is set (passed through to **`cmdSandbox`**).
24
+
25
+ **Note:** There is **no** `--baseline` flag on `gate` — baseline diff is **on by default** when a baseline file is present.
26
+
27
+ ## Exit codes
28
+
29
+ | Code | Meaning |
30
+ |------|---------|
31
+ | **0** | Check passed (no findings, no baseline **error** signals) and, if requested, sandbox **`ok`**. |
32
+ | **1** | Check failed **or** sandbox failed. |
33
+ | **2** | Thrown error from **`runCheck`** (e.g. missing `package.json` / lockfile). |
34
+
35
+ ## Output (`--json`)
36
+
37
+ - On check failure: `{ "gate": "failed", "stage": "check", ...fullResult }`
38
+ - On check success without sandbox: `{ "gate": "ok", "stage": "check", "packagesScanned": N }`
39
+ - Sandbox stage does not add JSON when **`--json`** was used (sandbox runs non-JSON).
40
+
41
+ ## How to test
42
+
43
+ ### Static gate only
44
+
45
+ ```bash
46
+ npx npm-sentinel gate --cwd /path/to/project
47
+ ```
48
+
49
+ With no baseline file: behaves like **`check`** without baseline. With baseline: includes diff signals.
50
+
51
+ ### Full gate with sandbox
52
+
53
+ ```bash
54
+ npx npm-sentinel gate --require-sandbox --no-build
55
+ ```
56
+
57
+ Requires Docker; static check must pass first.
58
+
59
+ ### Simulate failure
60
+
61
+ Introduce an IOC version or baseline **error** (e.g. **`new_dependency`**) and run **`gate`** — expect exit **1** before any sandbox runs.
62
+
63
+ ## When to use vs `check`
64
+
65
+ | Use **`check`** | Use **`gate`** |
66
+ |-----------------|----------------|
67
+ | Local dev, flexible flags (`--baseline` explicit, no baseline) | CI job that should always consider baseline if present |
68
+ | You never want sandbox | One command: **`gate --require-sandbox`** after tests |
69
+
70
+ If you need **`--baseline-file`** with **`gate`**, the current implementation may not support it on the **`runCheck`** path; use **`check --baseline --baseline-file …`** until the CLI is extended.
@@ -0,0 +1,28 @@
1
+ # Command: `help`
2
+
3
+ **Syntax:**
4
+
5
+ ```bash
6
+ npm-sentinel help
7
+ ```
8
+
9
+ **Note:** `npm-sentinel --help` is parsed as **flags** only; the CLI currently dispatches **`check`** before it evaluates `flags.help`, so **`--help` alone may still run `check`** in your tree. Prefer **`npm-sentinel help`** for the usage banner (or `npm-sentinel help` after we reorder dispatch if that gets fixed).
10
+
11
+ ## What it does
12
+
13
+ Prints a short reference listing **commands**, **flags**, **config** keys, and an example **`preinstall`** one-liner. **Does not** touch `package-lock.json` or network.
14
+
15
+ ## Exit code
16
+
17
+ **0**
18
+
19
+ ## How to test
20
+
21
+ ```bash
22
+ node bin/cli.js help
23
+ npx npm-sentinel help
24
+ ```
25
+
26
+ Expect the usage banner from `bin/cli.js` (~lines 263–291).
27
+
28
+ For full documentation, use this **`docs/`** tree and the [main README](../../README.md).
@@ -0,0 +1,101 @@
1
+ # Command: `sandbox`
2
+
3
+ **Syntax:** `npm-sentinel sandbox [flags]`
4
+
5
+ ## What it does
6
+
7
+ Runs a **real `npm ci`** inside **Docker** so install **lifecycle scripts** execute in an isolated Linux environment (not on your host). Optionally records **DNS** (UDP port 53) and enforces an **allowlist**.
8
+
9
+ Implementation: **`runSandbox`** in `lib/sandbox/docker-runner.js` + image `docker/Dockerfile.sandbox` + `docker/entrypoint-sandbox.sh`.
10
+
11
+ ### High-level flow
12
+
13
+ 1. Verifies **`package-lock.json`** exists at `--cwd`.
14
+ 2. If **`--mount-ssh`** / **`--ssh-dir`** / config SSH path: resolves mount; errors if path missing.
15
+ 3. Runs **`docker info`** — fails if Docker is unavailable.
16
+ 4. Unless **`--no-build`**, builds image **`npm-sentinel-sandbox:local`** from `docker/Dockerfile.sandbox`.
17
+ 5. **Runs container** with:
18
+ - Project mounted **read-only** at `/src`, copied to `/work`
19
+ - **`tcpdump`** on `udp port 53` appended to `/out/dns.log`
20
+ - **`npm ci`** in `/work` (stdout/stderr → `/out/npm.log`)
21
+ - Optional: mount host SSH keys for **`git+ssh`**
22
+ 6. After the container exits, the CLI reads **`dns.log`**, **`npm.log`**, **`npm-exit.code`**, runs **`extractHostsFromTcpdump`** + **`filterDisallowedHosts`** (`lib/sandbox/dns-parse.js`) using **`resolveDnsAllowlist`** (`lib/sandbox/docker-runner.js`): built-in defaults **`merge`**d with config, or config-only when **`dnsAllowlist.mode`** is **`replace`**.
23
+
24
+ ### Success condition
25
+
26
+ **`ok`** is true only when:
27
+
28
+ - **`npm ci`** exit code is **0**, **and**
29
+ - **No** extracted DNS hostname is outside the merged allowlist.
30
+
31
+ Either failure yields CLI exit **1** (unless preflight errors — see below).
32
+
33
+ ## Output (human)
34
+
35
+ - Preflight failure: **`res.error`** on stderr (lockfile, Docker, SSH path, docker build).
36
+ - Otherwise: **`npm ci exit:`** code, optional **DNS allowlist violations** list, optional npm log tail on install failure, **“Sandbox OK”** when `ok`.
37
+
38
+ ## Output (`--json`)
39
+
40
+ Object includes at least: **`ok`**, **`npmExit`**, **`npmLogTail`**, **`dnsHostsSample`**, **`disallowedDns`**, **`sshMounted`**, and on failure **`error`** may be set instead of npm/DNS fields.
41
+
42
+ Use **`dnsHostsSample`** to debug empty violations (parser/capture issues).
43
+
44
+ ## Exit codes
45
+
46
+ | Code | Meaning |
47
+ |------|---------|
48
+ | **0** | `res.ok` — install succeeded and DNS within allowlist. |
49
+ | **1** | DNS violations, `npm ci` failure, or any `ok: false` result. |
50
+ | *(preflight)* **1** | Same exit mapping: `res.ok` false includes `error` string cases. |
51
+
52
+ **Note:** The entrypoint exits with **`npm ci`’s code** inside the container; the **CLI** still treats non-zero npm or DNS violations as failure.
53
+
54
+ ## Flags
55
+
56
+ `--cwd`, `--json`, `--no-build`, `--mount-ssh`, `--ssh-dir` — see [../reference/flags.md](../reference/flags.md) and [../reference/config.md](../reference/config.md).
57
+
58
+ ## Limits & caveats
59
+
60
+ - **Linux image** (`node:20-bookworm-slim`): native addons built for macOS/Windows may **fail** `npm ci` here even when local install works.
61
+ - **DNS parsing** only recognizes certain **tcpdump** line shapes (`A?` / `AAAA?` / `CNAME?` patterns in `dns-parse.js`).
62
+ - **Broad default allowlist** (`lib/dns-allowlist-default.json`): malware using allowed CDNs may **not** trigger DNS violations.
63
+ - **`tcpdump` stderr** is discarded in the entrypoint; silent capture failures can yield **empty** host sets.
64
+
65
+ ## How to test
66
+
67
+ ### Happy path
68
+
69
+ ```bash
70
+ cd /path/to/small-js-project # valid package-lock.json
71
+ docker info # ensure daemon is up
72
+ npx npm-sentinel sandbox
73
+ # Second run:
74
+ npx npm-sentinel sandbox --no-build
75
+ ```
76
+
77
+ Expect **Sandbox OK** and exit **0**.
78
+
79
+ ### Force `npm ci` failure
80
+
81
+ Make `package.json` and `package-lock.json` **out of sync** (add a dependency in `package.json` without updating the lockfile). Run **`sandbox`** → expect non-zero **`npm ci exit`** and log tail.
82
+
83
+ ### Force DNS violation (controlled)
84
+
85
+ Add a root **`preinstall`** / **`postinstall`** that runs **`require('node:dns').resolve4('example.com', …)`**, run **`npm install`** to refresh the lockfile, then **`sandbox`**. If capture + parser see **`example.com`**, expect **DNS allowlist violations** (see `lib/dns-allowlist-default.json`).
86
+
87
+ Avoid **`dns.lookupSync`** on Node 25+ (removed); use **`resolve4`** or callback **`lookup`**.
88
+
89
+ ### Private git dependencies
90
+
91
+ ```bash
92
+ npx npm-sentinel sandbox --mount-ssh
93
+ # or
94
+ npx npm-sentinel sandbox --ssh-dir /path/to/deploy-keys
95
+ ```
96
+
97
+ See main README **Docker & SSH** / macOS **UseKeychain** notes.
98
+
99
+ ## Relation to `check`
100
+
101
+ **`sandbox`** does **not** run OSV or offline IOCs. Use **`check`** (or **`gate`**) for static findings; use **`sandbox`** when you need **install-time behavior** evidence.
@@ -0,0 +1,39 @@
1
+ # Configuration file
2
+
3
+ Optional. First matching file in the project root (`--cwd`) wins:
4
+
5
+ 1. `npm-sentinel.config.json`
6
+ 2. `.npm-sentinelsrc.json`
7
+
8
+ Parsed as JSON in `lib/config.js`. Invalid JSON yields an empty object for that file.
9
+
10
+ ## Keys
11
+
12
+ | Key | Type | Used by | Description |
13
+ |-----|------|---------|-------------|
14
+ | `watchPackagesExtra` | `string[]` | baseline, `check --baseline`, `gate` | Add package names to the **watch list** in addition to root `dependencies` / `devDependencies` / optional / peer deps. |
15
+ | `watchPackagesOverride` | `string[]` \| null | same | If set, **replace** the default watch list entirely (only these names are watched). |
16
+ | `dnsAllowlist` | object | `sandbox` | **`mode`** optional: **`merge`** (default) — defaults from `lib/dns-allowlist-default.json` **plus** your `suffixes` / `exactHosts`; **`replace`** — **only** your lists (no built-in suffixes/hosts). Invalid `mode` values behave as **`merge`**. |
17
+ | `sandbox.mountSsh` | boolean | `sandbox` | Same as CLI `--mount-ssh`. |
18
+ | `sandbox.sshDir` | string | `sandbox` | Same as `--ssh-dir`; wins over `mountSsh` when set. |
19
+
20
+ ## Example
21
+
22
+ ```json
23
+ {
24
+ "watchPackagesExtra": ["some-transitive-parent"],
25
+ "watchPackagesOverride": null,
26
+ "dnsAllowlist": {
27
+ "mode": "merge",
28
+ "suffixes": ["my-registry.example.com"],
29
+ "exactHosts": []
30
+ },
31
+ "sandbox": {
32
+ "mountSsh": true
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Strict allowlist (`replace`)
38
+
39
+ For a minimal allowlist (e.g. private registry only), set **`"mode": "replace"`** and list every **suffix** and **exact host** your `npm ci` legitimately resolves (often start by copying defaults from `lib/dns-allowlist-default.json`, then trim). Empty **`suffixes`** / **`exactHosts`** under **`replace`** allow **no** names by those rules until you add them.
@@ -0,0 +1,45 @@
1
+ # CLI flags reference
2
+
3
+ Parsed in `bin/cli.js` (`parseArgs`). Boolean flags can appear as `--flag` or `--flag=true`-style where noted.
4
+
5
+ ## All commands
6
+
7
+ | Flag | Type | Applies to | Description |
8
+ |------|------|------------|-------------|
9
+ | `--cwd <dir>` | path | all | Project root; must contain `package.json` (and usually `package-lock.json`). |
10
+ | `--json` | boolean | `check`, `baseline`, `sandbox`, `gate` | Machine-readable JSON on stdout (shape varies by command). |
11
+ | `--help` / `help` | — | `npm-sentinel help` | Prints usage and exits 0. |
12
+
13
+ ## `check` and `gate`
14
+
15
+ | Flag | Default | Description |
16
+ |------|---------|-------------|
17
+ | `--min-severity` | `low` | Filter findings: `low`, `moderate`, `high`, `critical`. |
18
+ | `--no-osv` | off | Do not call the OSV batch API. |
19
+ | `--offline` | off | Skip OSV entirely (offline IOCs in `lib/offline-iocs.json` still apply). |
20
+ | `--baseline` | off | **`check` only:** if **`.npm-sentinel-baseline.json`** exists, run baseline diff and merge signals into output. Custom paths are **not** wired into `runCheck` / **`gate`**. |
21
+ | `--npm-audit` | off | Run `npm audit --json` in the project and attach summary to results (**does not** change exit code by itself; see [check.md](../commands/check.md)). |
22
+
23
+ ## `gate` only
24
+
25
+ | Flag | Description |
26
+ |------|-------------|
27
+ | `--require-sandbox` | After a successful check, run **`sandbox`** (Docker). **`gate` forces `json: false` for the sandbox stage** even if `--json` was passed. |
28
+
29
+ ## `sandbox` only
30
+
31
+ | Flag | Description |
32
+ |------|-------------|
33
+ | `--no-build` | Skip `docker build`; reuse existing `npm-sentinel-sandbox:local` image. |
34
+ | `--mount-ssh` | Mount host `~/.ssh` read-only at `/root/.ssh` in the container. |
35
+ | `--ssh-dir <path>` | Mount this directory instead of `~/.ssh` (for `git+ssh` deps). |
36
+
37
+ Config can also set `sandbox.mountSsh`, `sandbox.sshDir`, and `dnsAllowlist` (see [config.md](config.md)).
38
+
39
+ ## `baseline` subcommands
40
+
41
+ | Flag | Description |
42
+ |------|-------------|
43
+ | `--baseline-file <path>` | Read/write baseline JSON at this path instead of **`.npm-sentinel-baseline.json`**. |
44
+
45
+ `baseline save` and `baseline diff` also accept `--cwd` and `--json`. (**Not** passed through from `check` / `gate` today.)
@@ -0,0 +1,19 @@
1
+ # Testing npm-sentinel
2
+
3
+ End-to-end ideas are split by command so they stay next to behavior and exit codes:
4
+
5
+ | Goal | Doc |
6
+ |------|-----|
7
+ | Static scan, OSV, IOCs, severity, `npm audit` | [commands/check.md — How to test](commands/check.md#how-to-test) |
8
+ | Baseline save/diff, signal types | [commands/baseline.md — How to test](commands/baseline.md#how-to-test) |
9
+ | Docker `npm ci`, DNS allowlist, SSH | [commands/sandbox.md — How to test](commands/sandbox.md#how-to-test) |
10
+ | CI gate + optional sandbox | [commands/gate.md — How to test](commands/gate.md#how-to-test) |
11
+
12
+ **This repo’s automated tests:**
13
+
14
+ ```bash
15
+ npm ci
16
+ npm test
17
+ ```
18
+
19
+ Fixtures live under **`test/fixtures/`** (e.g. lockfile mini IOC case in **`cli-smoke.test.js`**).
@@ -0,0 +1,83 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ parseNpmLockfile,
5
+ getRootDependencyResolution,
6
+ } from "./parse-npm-lockfile.js";
7
+ import { fetchVersionScripts, summarizeLifecycleScripts } from "./packument.js";
8
+
9
+ export const DEFAULT_BASELINE_PATH = ".npm-sentinel-baseline.json";
10
+
11
+ /**
12
+ * @param {string} cwd
13
+ * @param {string} [relPath]
14
+ */
15
+ export function baselinePath(cwd, relPath = DEFAULT_BASELINE_PATH) {
16
+ return join(cwd, relPath);
17
+ }
18
+
19
+ /**
20
+ * @param {object} pkg
21
+ * @param {ReturnType<import('./parse-npm-lockfile.js').parseNpmLockfile>} parsed
22
+ * @param {string[]} watchNames
23
+ * @param {typeof fetch} [fetchImpl]
24
+ */
25
+ export async function buildBaselineSnapshot(
26
+ pkg,
27
+ parsed,
28
+ watchNames,
29
+ fetchImpl = fetch
30
+ ) {
31
+ /** @type {Record<string, object>} */
32
+ const packages = {};
33
+
34
+ for (const name of watchNames) {
35
+ const res = getRootDependencyResolution(parsed, name);
36
+ if (!res) {
37
+ packages[name] = {
38
+ resolvedVersion: null,
39
+ directDependencyNames: [],
40
+ lifecycleScriptKeys: [],
41
+ scriptHashes: {},
42
+ missingInLockfile: true,
43
+ };
44
+ continue;
45
+ }
46
+
47
+ const scripts = await fetchVersionScripts(name, res.version, fetchImpl);
48
+ const sum =
49
+ scripts === null
50
+ ? { keys: [], hashes: {} }
51
+ : summarizeLifecycleScripts(scripts);
52
+
53
+ packages[name] = {
54
+ resolvedVersion: res.version,
55
+ directDependencyNames: [...(res.directDependencyNames || [])].sort(),
56
+ lifecycleScriptKeys: sum.keys.sort(),
57
+ scriptHashes: sum.hashes,
58
+ missingInLockfile: false,
59
+ };
60
+ }
61
+
62
+ return {
63
+ version: 1,
64
+ savedAt: new Date().toISOString(),
65
+ packages,
66
+ };
67
+ }
68
+
69
+ export function loadBaseline(cwd, relPath = DEFAULT_BASELINE_PATH) {
70
+ const p = baselinePath(cwd, relPath);
71
+ if (!existsSync(p)) return null;
72
+ try {
73
+ return JSON.parse(readFileSync(p, "utf8"));
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ export function saveBaseline(snapshot, cwd, relPath = DEFAULT_BASELINE_PATH) {
80
+ const p = baselinePath(cwd, relPath);
81
+ writeFileSync(p, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
82
+ return p;
83
+ }
package/lib/config.js ADDED
@@ -0,0 +1,49 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const CONFIG_NAMES = [
5
+ "npm-sentinel.config.json",
6
+ ".npm-sentinelsrc.json",
7
+ ];
8
+
9
+ /**
10
+ * @param {string} cwd
11
+ * @returns {object}
12
+ */
13
+ export function loadConfig(cwd) {
14
+ for (const name of CONFIG_NAMES) {
15
+ const p = join(cwd, name);
16
+ if (existsSync(p)) {
17
+ try {
18
+ return JSON.parse(readFileSync(p, "utf8"));
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+ }
24
+ return {};
25
+ }
26
+
27
+ /**
28
+ * @param {object} pkg - package.json parsed
29
+ * @param {object} config
30
+ * @returns {string[]}
31
+ */
32
+ export function getWatchPackageNames(pkg, config) {
33
+ if (Array.isArray(config.watchPackagesOverride)) {
34
+ return [...config.watchPackagesOverride].sort();
35
+ }
36
+ const extra = config.watchPackagesExtra || [];
37
+ const root = new Set(
38
+ [
39
+ ...Object.keys(pkg.dependencies || {}),
40
+ ...Object.keys(pkg.devDependencies || {}),
41
+ ...Object.keys(pkg.optionalDependencies || {}),
42
+ ...Object.keys(pkg.peerDependencies || {}),
43
+ ].filter(Boolean)
44
+ );
45
+ if (Array.isArray(extra)) {
46
+ for (const e of extra) root.add(e);
47
+ }
48
+ return [...root].sort();
49
+ }