@clear-capabilities/agentic-security-scanner 0.124.1 → 0.127.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.
- package/CHANGELOG.md +146 -0
- package/bin/agentic-security.js +75 -2
- package/dist/178.index.js +1 -1
- package/dist/220.index.js +193 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +2395 -0
- package/dist/449.index.js +135 -0
- package/dist/637.index.js +1 -1
- package/dist/752.index.js +7 -4
- package/dist/801.index.js +87 -0
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +5 -5
- package/src/engine.js +4 -0
- package/src/ir/CLAUDE.md +22 -17
- package/src/llm-validator/index.js +47 -12
- package/src/mcp/tools.js +97 -3
- package/src/posture/CLAUDE.md +3 -1
- package/src/posture/cache-economics.js +7 -4
- package/src/posture/deterministic-fix.js +65 -0
- package/src/posture/mttr.js +25 -0
- package/src/posture/provider-catalog.js +108 -0
- package/src/posture/secret-live-check.js +71 -0
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/api-authz.js +36 -0
- package/src/sast/file-upload.js +118 -0
- package/src/sast/llm-cost-advisor.js +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,151 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.127.0 — cost advisor: an actual choice, not just a tip
|
|
4
|
+
|
|
5
|
+
The model-cost advisor (`hooks/model-cost-advisor.js`) has always been advisory
|
|
6
|
+
only — a `systemMessage` tip you read and act on yourself, by design, because a
|
|
7
|
+
`UserPromptSubmit` hook cannot pause for interactive input or change the model/
|
|
8
|
+
effort (verified against the current Claude Code hooks reference before writing
|
|
9
|
+
any of this, not assumed from the hook's own pre-existing comments). This
|
|
10
|
+
release adds a real choice on top of that hard limit, without giving up the
|
|
11
|
+
zero-token guarantee for anyone who doesn't opt in.
|
|
12
|
+
|
|
13
|
+
- **New opt-in `"interactive": true`** (`.agentic-security/model-optimizer.json`,
|
|
14
|
+
default `false`). On a qualifying prompt, the hook now additionally emits
|
|
15
|
+
`hookSpecificOutput.additionalContext` — billed as input tokens, unlike the
|
|
16
|
+
free `systemMessage` — directing Claude to call `AskUserQuestion` with three
|
|
17
|
+
options: keep current defaults; get the exact `/model`/`/effort` command to
|
|
18
|
+
run yourself (Claude cannot switch its own running model — no exception
|
|
19
|
+
exists); or apply the cheaper model/effort to Claude's own delegated
|
|
20
|
+
Task/Agent-tool sub-agent dispatches for the rest of the session — the one
|
|
21
|
+
axis Claude can genuinely act on directly. Every other install keeps today's
|
|
22
|
+
zero-token behavior unchanged; only projects that explicitly opt in pay for
|
|
23
|
+
the interactive path, and only on prompts that already qualify for a tip.
|
|
24
|
+
- The chosen sub-agent override is **sticky for the session** — persisted to
|
|
25
|
+
`.agentic-security/model-optimizer-state.json` (extending the existing state
|
|
26
|
+
file rather than adding a new one) and cleared at the next `SessionStart`,
|
|
27
|
+
which already does a bare overwrite of that file. A cooldown
|
|
28
|
+
(`interactiveCooldownTurns`, default 3) stops the directive from re-firing
|
|
29
|
+
its token cost if Claude doesn't act on it (e.g. a non-interactive/scripted
|
|
30
|
+
invocation).
|
|
31
|
+
- Fixed a real bug the new shape would otherwise have hit: `dispatch-user-
|
|
32
|
+
prompt.js`'s `mergeOutputs` previously let the legacy-alias-redirect's
|
|
33
|
+
`additionalContext` and the advisor's new `additionalContext` clobber each
|
|
34
|
+
other if both fired on the same prompt (independent triggers, so this can
|
|
35
|
+
genuinely co-occur) — now joined instead of last-write-wins.
|
|
36
|
+
- `commands/setup.md`'s `--model-optimizer` gained a matching `--interactive`
|
|
37
|
+
flag; `CLAUDE.md` documents the durable instruction for applying a saved
|
|
38
|
+
sub-agent override on future dispatches, respecting existing static
|
|
39
|
+
`model:` frontmatter pins (`security-triager.md`, `sca-triager.md`) and the
|
|
40
|
+
"task needs more capability" carve-out.
|
|
41
|
+
|
|
42
|
+
Also added `SECURITY.md` (vulnerability disclosure policy).
|
|
43
|
+
|
|
44
|
+
## 0.126.0 — closing the find→fix loop: verified auto-fix, CWE-434/306, Fable 5 pricing
|
|
45
|
+
|
|
46
|
+
The largest remediation-side release to date. Prior to this, findings could be
|
|
47
|
+
*found* at scale but not *fixed* at scale — of the 677 findings on this repo's own
|
|
48
|
+
scan, zero could be mechanically applied. This release closes that loop end to end
|
|
49
|
+
while adding the detection classes and workflow polish the closed loop needed.
|
|
50
|
+
Full detail and rationale: `docs/FIND_AND_FIX_LOOP_PRD.md` (implemented; the file
|
|
51
|
+
itself is removed post-implementation per its own instructions).
|
|
52
|
+
|
|
53
|
+
**Remediation — a verified patch path for every finding, not just the ones with a
|
|
54
|
+
stored replacement.**
|
|
55
|
+
- `apply_fix` (MCP) now accepts a caller-supplied `patch` (a files map) and
|
|
56
|
+
**re-verifies it inline** — rescan-clean, no new ≥medium finding, lint-clean —
|
|
57
|
+
before writing. Previously `apply_fix` refused any finding without a stored
|
|
58
|
+
`fix.replacement`; now a template-only or description-only finding (the vast
|
|
59
|
+
majority) can be fixed, because the bytes are proven safe at write time instead
|
|
60
|
+
of trusted at synthesis time.
|
|
61
|
+
- **Deterministic zero-LLM patch synthesis** (`posture/deterministic-fix.js`) for
|
|
62
|
+
safe, context-independent classes — weak hash (md5/sha1→sha256), TLS
|
|
63
|
+
verify-off→on — materialized on demand by `synthesize_fix` and still gated by
|
|
64
|
+
the same inline verifier before `apply_fix` writes it.
|
|
65
|
+
- Regression tests are wired into the fix loop: `synthesize_fix` surfaces the
|
|
66
|
+
scan's existing PoC-derived `regression_test` so a fix ships with a test that
|
|
67
|
+
fails pre-fix and passes post-fix.
|
|
68
|
+
- `/fix --all` and `/find-and-fix-everything` now run independent findings in
|
|
69
|
+
**parallel** (serializing only same-file findings), never halt on the first
|
|
70
|
+
test failure, and publish the running **auto-fix acceptance rate**.
|
|
71
|
+
- `/fix --sca` surfaces its **upgrade break-rate** (build/test-verified upgrades
|
|
72
|
+
are the default; the break-rate is how often an "available" upgrade actually
|
|
73
|
+
wasn't safe to take).
|
|
74
|
+
- MTTR / SLA tracking is now live: every scan stamps `firstSeenAt`/`ageDays` and
|
|
75
|
+
surfaces an SLA-breach summary (`critical` 7d / `high` 30d / …).
|
|
76
|
+
- The Layer-3 LLM validator gained a first-class Anthropic preset
|
|
77
|
+
(`AGENTIC_SECURITY_LLM_PRESET=anthropic`) — previously BYO-endpoint only, so the
|
|
78
|
+
FP-suppression layer was reachable with just a key instead of a hand-built
|
|
79
|
+
endpoint.
|
|
80
|
+
|
|
81
|
+
**Detection.**
|
|
82
|
+
- New **CWE-434 unrestricted file-upload detector** (JS/Python) — a whole CWE
|
|
83
|
+
class that had zero coverage: unguarded Multer configs and writes that use the
|
|
84
|
+
client-supplied filename as the destination.
|
|
85
|
+
- New **CWE-306 missing-authentication** rule: an unauthenticated destructive
|
|
86
|
+
route (DELETE, or id-taking PUT/PATCH) fires only when the app authenticates
|
|
87
|
+
elsewhere in the codebase — so auth-detection is proven to work before the rule
|
|
88
|
+
trusts a "no auth found" signal, keeping it high-precision even on all-public
|
|
89
|
+
files.
|
|
90
|
+
- Corrected stale documentation in `scanner/src/ir/CLAUDE.md`: the Python CST
|
|
91
|
+
parser's `match`-case bodies, walrus bindings, destructuring, and comprehension
|
|
92
|
+
filters were already fully lowered and taint-propagating — verified end-to-end
|
|
93
|
+
with new flow tests, not just parser-shape tests.
|
|
94
|
+
- The independent-eval gate (`bench/independent-eval/`) is now **active**
|
|
95
|
+
(`aggregateF1`/`perFamilyRecall` floors instead of `null`) — proven to fail on a
|
|
96
|
+
deliberate regression and pass at the current corpus result.
|
|
97
|
+
|
|
98
|
+
**Hooks & cost.**
|
|
99
|
+
- Pricing tables across all five rate-table copies now include **Fable 5
|
|
100
|
+
($10/$50 per MTok)** and Sonnet 5 — previously a Fable 5 session wasn't even
|
|
101
|
+
priced by the cache-economics reporter, and the cost advisor couldn't reason
|
|
102
|
+
about the flagship model at all.
|
|
103
|
+
- Two mechanical subagents (`security-triager`, `sca-triager`) pinned to Haiku.
|
|
104
|
+
- The two `UserPromptSubmit` hooks and three `PreToolUse` (Edit) hooks were each
|
|
105
|
+
consolidated into a single dispatcher process — halving/thirding the per-turn
|
|
106
|
+
node spawns. The security-critical bodyguard block is proven to survive
|
|
107
|
+
consolidation (a dedicated regression test asserts the exit-2 deny still
|
|
108
|
+
fires first and short-circuits the advisory hooks).
|
|
109
|
+
- The post-edit hook now offers a one-tap auto-fix inline when a fresh finding is
|
|
110
|
+
mechanically fixable, instead of only pointing at `/fix-all`.
|
|
111
|
+
- The model-cost optimizer now ships **on by default** (`mode: "advise"`) with a
|
|
112
|
+
predicted-vs-realized savings ledger.
|
|
113
|
+
|
|
114
|
+
**Workflow.**
|
|
115
|
+
- `scan --watch` wires the existing (previously unwired) watch-mode daemon:
|
|
116
|
+
continuous incremental re-scan on file change with a risk-delta status line.
|
|
117
|
+
- Opt-in, offline-degrading **live-secret validation**
|
|
118
|
+
(`--validate-secrets`) — labels a detected secret `live`/`dead`/`unknown` via a
|
|
119
|
+
read-only provider "whoami" check (GitHub, Stripe, OpenAI, SendGrid), so "this
|
|
120
|
+
key is LIVE" is distinguished from "you have a high-entropy string."
|
|
121
|
+
`--secret-history` (git-history sweep) was already wired; this release adds
|
|
122
|
+
the liveness half.
|
|
123
|
+
- Diff-scoped scans (`--pr` / `--changed-since`) now default `--incremental` on,
|
|
124
|
+
since a changed-file set is exactly the incremental cache's designed case.
|
|
125
|
+
|
|
126
|
+
## 0.125.0 — multi-provider LLM cost + prompt-cache linter (Cache Economics v2, phase 1)
|
|
127
|
+
|
|
128
|
+
First slice of the Cache Economics v2 PRD (`docs/CACHE_ECONOMICS_V2_PRD.md`) — the
|
|
129
|
+
foundation (P1 provider detection, P2 provider catalog) + the flagship analyzer (F1
|
|
130
|
+
cache-hygiene + P3 per-provider model/depth recommendation). The optimizer now reaches
|
|
131
|
+
beyond this Claude Code session into the user's **own AI-app code**, across providers.
|
|
132
|
+
|
|
133
|
+
- **Provider catalog** (`scanner/src/posture/provider-catalog.js`): a dated, no-network
|
|
134
|
+
snapshot of Anthropic / OpenAI / Google Gemini / xAI — model ladder ($/1M rates), the
|
|
135
|
+
"depth" knob (effort / reasoning_effort / thinkingBudget), and the cache model
|
|
136
|
+
(explicit vs automatic vs implicit). Prices are sourced, not hardcoded-in-logic, with a
|
|
137
|
+
`SOURCED_AT` staleness date.
|
|
138
|
+
- **LLM cost/cache detector** (`scanner/src/sast/llm-cost-advisor.js`, `scanLlmCost`):
|
|
139
|
+
provider-aware, gated on detecting an LLM SDK (low FP), emits **advisory** findings —
|
|
140
|
+
(1) *prompt-cache killer*: a timestamp/UUID/random value baked into a prompt prefix
|
|
141
|
+
that defeats caching; (2) *over-provisioned*: a flagship model at high depth, with the
|
|
142
|
+
catalog's cheaper model + lower depth **in that provider's framework** as the fix
|
|
143
|
+
(e.g. an OpenAI app → "gpt-5.4 at reasoning_effort=low"). Severity `low`/`info` so it
|
|
144
|
+
never inflates security counts.
|
|
145
|
+
|
|
146
|
+
Remaining PRD phases (F2 measured cost, F3 TTL, F4 compaction, F5 pre-warm, F6 cross-
|
|
147
|
+
session warmth, F7 self-tuning; full P4 per-provider economics) ship next.
|
|
148
|
+
|
|
3
149
|
## 0.124.1 — fix: narration no longer prints a broken location
|
|
4
150
|
|
|
5
151
|
Now that v0.124.0 surfaces `narration` prominently inline, a pre-existing template
|
package/bin/agentic-security.js
CHANGED
|
@@ -113,6 +113,7 @@ Options:
|
|
|
113
113
|
--since-baseline Only show findings NOT in the saved baseline
|
|
114
114
|
--hide-proven-safe Drop findings discharged by a flow proof (provably safe)
|
|
115
115
|
--secret-history Also sweep recent git history for committed secrets
|
|
116
|
+
--validate-secrets Label each detected secret live/dead/unknown (opt-in, network, offline-degrading)
|
|
116
117
|
(removed from HEAD but recoverable from .git)
|
|
117
118
|
--history-depth <n> Commits to sweep with --secret-history (default 50)
|
|
118
119
|
--no-epss Skip EPSS exploit-prediction enrichment (default: enabled)
|
|
@@ -331,8 +332,40 @@ async function cmdScan(args) {
|
|
|
331
332
|
}
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
// #21 — --watch : continuous incremental re-scan on file change. Each change
|
|
336
|
+
// re-scans (incrementally) and writes a risk-delta to
|
|
337
|
+
// .agentic-security/watch-status.md that a statusline / /posture poll surfaces
|
|
338
|
+
// inline — so "did my edit add risk?" is answered without a manual re-scan.
|
|
339
|
+
// Blocks until Ctrl-C, like `jest --watch`. Opt-in, so it never affects a
|
|
340
|
+
// normal one-shot scan.
|
|
341
|
+
if (args.flags['watch']) {
|
|
342
|
+
process.env.AGENTIC_SECURITY_INCREMENTAL = '1';
|
|
343
|
+
const { watchProject, computeDelta, persistStatus, renderStatusLine } = await import('../src/posture/watch-mode.js');
|
|
344
|
+
process.stderr.write(`[watch] scanning ${targetAbs} on change — Ctrl-C to stop. Status → .agentic-security/watch-status.md\n`);
|
|
345
|
+
const seed = await runScan(targetAbs, {});
|
|
346
|
+
let prevFindings = seed.scan.findings || [];
|
|
347
|
+
await watchProject(targetAbs, async () => {
|
|
348
|
+
try {
|
|
349
|
+
const { scan } = await runScan(targetAbs, {});
|
|
350
|
+
const curr = scan.findings || [];
|
|
351
|
+
const delta = computeDelta(prevFindings, curr);
|
|
352
|
+
persistStatus(targetAbs, delta);
|
|
353
|
+
process.stderr.write('[watch] ' + renderStatusLine(delta) + '\n');
|
|
354
|
+
prevFindings = curr;
|
|
355
|
+
} catch (e) {
|
|
356
|
+
process.stderr.write(`[watch] rescan failed: ${e.message}\n`);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return 0; // watchProject blocks until aborted
|
|
360
|
+
}
|
|
361
|
+
|
|
334
362
|
// --incremental : reuse taint summaries from prior scans for faster deep mode.
|
|
335
|
-
|
|
363
|
+
// #23 — default incremental ON for diff-scoped scans (--pr / --changed-since):
|
|
364
|
+
// that's the cache's designed use (small changed set → its callers), so the
|
|
365
|
+
// PR-native path is fast by default. A full-tree scan stays non-incremental
|
|
366
|
+
// unless explicitly requested (blanket-default flip needs broader validation).
|
|
367
|
+
const _diffScoped = !!(args.flags['pr'] || args.flags['changed-since']);
|
|
368
|
+
if (args.flags['incremental'] || process.env.AGENTIC_SECURITY_INCREMENTAL === '1' || _diffScoped) {
|
|
336
369
|
process.env.AGENTIC_SECURITY_INCREMENTAL = '1';
|
|
337
370
|
}
|
|
338
371
|
|
|
@@ -566,6 +599,46 @@ async function cmdScan(args) {
|
|
|
566
599
|
if (_isSafeStateDir(stateDir)) {
|
|
567
600
|
await fsp.mkdir(stateDir, { recursive: true });
|
|
568
601
|
const persistedScan = toJSON(scan, meta);
|
|
602
|
+
// #10 — MTTR: stamp firstSeenAt/lastSeenAt/ageDays from the PREVIOUS scan so
|
|
603
|
+
// every finding carries an age, SLA breaches can be surfaced, and the fix
|
|
604
|
+
// loop can report time-to-clean. Best-effort; skipped under --deterministic
|
|
605
|
+
// so deterministic state stays byte-identical run-to-run.
|
|
606
|
+
if (!args.flags.deterministic) {
|
|
607
|
+
try {
|
|
608
|
+
const { stampFindingTimestamps, buildBaselineMap, renderSlaSummary } = await import('../src/posture/mttr.js');
|
|
609
|
+
let baselineMap = new Map();
|
|
610
|
+
try {
|
|
611
|
+
const prev = JSON.parse(await fsp.readFile(path.join(stateDir, 'last-scan.json'), 'utf8'));
|
|
612
|
+
baselineMap = buildBaselineMap(prev);
|
|
613
|
+
} catch { /* first run — empty baseline, everything is firstSeen now */ }
|
|
614
|
+
const now = Date.now();
|
|
615
|
+
stampFindingTimestamps(persistedScan.findings || [], baselineMap, now);
|
|
616
|
+
stampFindingTimestamps(persistedScan.secrets || [], baselineMap, now);
|
|
617
|
+
stampFindingTimestamps((persistedScan.supplyChain || []).filter(s => s.type === 'vulnerable_dep'), baselineMap, now);
|
|
618
|
+
// Surface the SLA-breach line on human-readable formats (not JSON/CI pipes).
|
|
619
|
+
const isJson = format === 'json' || format === 'sarif' || format === 'cyclonedx' || format === 'sbom' || format === 'spdx' || format === 'vex' || format === 'openvex' || format === 'pbom' || format === 'aibom';
|
|
620
|
+
if (!isJson) {
|
|
621
|
+
const sla = renderSlaSummary(persistedScan.findings || []);
|
|
622
|
+
if (sla) process.stderr.write(`⏰ agentic-security: ${sla}\n`);
|
|
623
|
+
}
|
|
624
|
+
} catch { /* MTTR is best-effort — never block a scan write */ }
|
|
625
|
+
}
|
|
626
|
+
// #22 — live-secret validation (opt-in, offline-degrading). Label each
|
|
627
|
+
// detected secret live | dead | unknown via a read-only provider "whoami".
|
|
628
|
+
// "This key is LIVE and was committed N commits ago" is the P0 that matters.
|
|
629
|
+
if (args.flags['validate-secrets'] || process.env.AGENTIC_SECURITY_VALIDATE_SECRETS === '1') {
|
|
630
|
+
try {
|
|
631
|
+
const { checkSecretLive } = await import('../src/posture/secret-live-check.js');
|
|
632
|
+
let live = 0;
|
|
633
|
+
for (const s of (persistedScan.secrets || [])) {
|
|
634
|
+
const { verdict, provider } = await checkSecretLive(s);
|
|
635
|
+
s.liveVerdict = verdict;
|
|
636
|
+
if (provider) s.liveProvider = provider;
|
|
637
|
+
if (verdict === 'live') live++;
|
|
638
|
+
}
|
|
639
|
+
if (live > 0) process.stderr.write(`🔴 agentic-security: ${live} LIVE secret(s) validated — rotate immediately (even if already removed from HEAD).\n`);
|
|
640
|
+
} catch { /* best-effort, offline-degrading — never block a scan */ }
|
|
641
|
+
}
|
|
569
642
|
const lastScanBody = JSON.stringify(persistedScan, null, 2);
|
|
570
643
|
await fsp.writeFile(path.join(stateDir, 'last-scan.json'), lastScanBody);
|
|
571
644
|
try {
|
|
@@ -1535,7 +1608,7 @@ description: Remediate every finding at or above a severity threshold (default:
|
|
|
1535
1608
|
argument-hint: "[--severity critical|high|medium]"
|
|
1536
1609
|
---
|
|
1537
1610
|
|
|
1538
|
-
Read \`.agentic-security/last-scan.json\`. For every finding at or above \`\${1:-critical}\` severity, dispatch the security-fixer subagent
|
|
1611
|
+
Read \`.agentic-security/last-scan.json\`. For every finding at or above \`\${1:-critical}\` severity, dispatch the security-fixer subagent — independent findings in parallel, serializing only findings that share a file. Each fix is inline-verified by apply_fix before it lands (finding gone + no new ≥medium + lint). Do NOT halt on the first failure: record each finding's outcome and continue, then report the full list and the auto-fix acceptance rate. Re-run \`/security-scan-all\` to confirm.
|
|
1539
1612
|
`,
|
|
1540
1613
|
'security-report.md': `---
|
|
1541
1614
|
description: Generate an HTML security report (or JSON / Markdown / SARIF).
|
package/dist/178.index.js
CHANGED
|
@@ -13,7 +13,7 @@ export const modules = {
|
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
14
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
15
15
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
16
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
16
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3793);
|
|
17
17
|
// Time-travel + counterfactual scanning (v0.68).
|
|
18
18
|
//
|
|
19
19
|
// Two new modes that exploit the pure-input shape of runFullScan:
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export const id = 220;
|
|
2
|
+
export const ids = [220];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 9220:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ computeDelta: () => (/* binding */ computeDelta),
|
|
10
|
+
/* harmony export */ persistStatus: () => (/* binding */ persistStatus),
|
|
11
|
+
/* harmony export */ renderStatusLine: () => (/* binding */ renderStatusLine),
|
|
12
|
+
/* harmony export */ watchProject: () => (/* binding */ watchProject)
|
|
13
|
+
/* harmony export */ });
|
|
14
|
+
/* unused harmony exports readStatus, _internals */
|
|
15
|
+
/* harmony import */ var node_fs_promises__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1455);
|
|
16
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
17
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
18
|
+
// Watch mode — continuous incremental scan as the developer edits.
|
|
19
|
+
//
|
|
20
|
+
// Spawns a long-running scan watcher that:
|
|
21
|
+
// 1. Subscribes to file-system events under the project root
|
|
22
|
+
// 2. When a file matching the scan glob changes, re-scans incrementally
|
|
23
|
+
// using dataflow/incremental-cache.js (per-file cache hits avoid the
|
|
24
|
+
// full IR build)
|
|
25
|
+
// 3. Diffs against the prior scan to compute a "risk delta" string
|
|
26
|
+
// (added/removed/changed criticals + highs)
|
|
27
|
+
// 4. Writes the delta to .agentic-security/watch-status.{md,json}
|
|
28
|
+
//
|
|
29
|
+
// The Claude Code statusline / a chat command can poll watch-status.md
|
|
30
|
+
// (cheap file read) to surface the delta inline without re-running anything.
|
|
31
|
+
//
|
|
32
|
+
// Implementation is deliberately node-only — no chokidar dep, uses
|
|
33
|
+
// fs.promises.watch (Node ≥ 20). Debounces to 350ms.
|
|
34
|
+
//
|
|
35
|
+
// Lifecycle: start() returns an AbortController-like handle. The caller
|
|
36
|
+
// (a /watch slash command or a long-running daemon spawn) is responsible
|
|
37
|
+
// for keeping the process alive; this module is the pure logic.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
const STATE = '.agentic-security';
|
|
44
|
+
const STATUS_MD = 'watch-status.md';
|
|
45
|
+
const STATUS_JSON = 'watch-status.json';
|
|
46
|
+
const DEBOUNCE_MS = 350;
|
|
47
|
+
const MAX_BURST = 50; // ignore beyond this # of rapid events
|
|
48
|
+
|
|
49
|
+
const SCAN_EXT_RE = /\.(?:[jt]sx?|mjs|cjs|py|java|kt|go|rb|php|cs|c|cc|cpp|h|hpp|rs|sol|vy|swift|dart|toml|yml|yaml|json|tf|tfvars|bicep)$/i;
|
|
50
|
+
const IGNORE_DIR_RE = /(?:^|\/)(?:\.git|node_modules|\.bench-cache|dist|build|\.next|coverage|\.agentic-security)(?:$|\/)/;
|
|
51
|
+
|
|
52
|
+
function _isScanable(rel) {
|
|
53
|
+
if (!rel || IGNORE_DIR_RE.test(rel)) return false;
|
|
54
|
+
return SCAN_EXT_RE.test(rel);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _readJsonSafe(fp) {
|
|
58
|
+
try { return JSON.parse(fsSync.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _sevRank(s) { return ['info', 'low', 'medium', 'high', 'critical'].indexOf(s) + 1; }
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Pure delta computation between two finding arrays. Same logic
|
|
65
|
+
* baseline-compare uses, surfaced as a single ASCII status line.
|
|
66
|
+
*/
|
|
67
|
+
function computeDelta(prevFindings, currFindings) {
|
|
68
|
+
const key = (f) => `${f.file || ''}::${f.line || 0}::${f.family || f.parser || ''}`;
|
|
69
|
+
const prev = new Map();
|
|
70
|
+
for (const f of prevFindings || []) prev.set(key(f), f);
|
|
71
|
+
const cur = new Map();
|
|
72
|
+
for (const f of currFindings || []) cur.set(key(f), f);
|
|
73
|
+
const added = [], removed = [];
|
|
74
|
+
for (const [k, f] of cur) if (!prev.has(k)) added.push(f);
|
|
75
|
+
for (const [k, f] of prev) if (!cur.has(k)) removed.push(f);
|
|
76
|
+
const newCrit = added.filter(f => f.severity === 'critical').length;
|
|
77
|
+
const newHigh = added.filter(f => f.severity === 'high').length;
|
|
78
|
+
const fixedCrit = removed.filter(f => f.severity === 'critical').length;
|
|
79
|
+
const fixedHigh = removed.filter(f => f.severity === 'high').length;
|
|
80
|
+
return {
|
|
81
|
+
addedCount: added.length, removedCount: removed.length,
|
|
82
|
+
newCritical: newCrit, newHigh, fixedCritical: fixedCrit, fixedHigh,
|
|
83
|
+
added, removed,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render a one-line status string for the Claude Code statusline.
|
|
89
|
+
*/
|
|
90
|
+
function renderStatusLine(delta) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (delta.newCritical) parts.push(`🛑 +${delta.newCritical} crit`);
|
|
93
|
+
if (delta.newHigh) parts.push(`⚠️ +${delta.newHigh} high`);
|
|
94
|
+
if (delta.fixedCritical) parts.push(`✅ -${delta.fixedCritical} crit`);
|
|
95
|
+
if (delta.fixedHigh) parts.push(`✅ -${delta.fixedHigh} high`);
|
|
96
|
+
if (!parts.length && (delta.addedCount + delta.removedCount) === 0) return 'agentic-security: clean';
|
|
97
|
+
if (!parts.length) return `agentic-security: +${delta.addedCount} / -${delta.removedCount}`;
|
|
98
|
+
return 'agentic-security: ' + parts.join(' · ');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Persist watch-status.{md,json}. Cheap atomic write (write tmp, rename).
|
|
103
|
+
*/
|
|
104
|
+
function persistStatus(scanRoot, delta) {
|
|
105
|
+
const dir = node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, STATE);
|
|
106
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(dir, { recursive: true }); } catch {}
|
|
107
|
+
const status = {
|
|
108
|
+
ts: new Date().toISOString(),
|
|
109
|
+
line: renderStatusLine(delta),
|
|
110
|
+
delta: {
|
|
111
|
+
addedCount: delta.addedCount, removedCount: delta.removedCount,
|
|
112
|
+
newCritical: delta.newCritical, newHigh: delta.newHigh,
|
|
113
|
+
fixedCritical: delta.fixedCritical, fixedHigh: delta.fixedHigh,
|
|
114
|
+
},
|
|
115
|
+
addedTop5: (delta.added || []).slice(0, 5).map(f => ({
|
|
116
|
+
file: f.file, line: f.line, family: f.family, severity: f.severity, vuln: f.vuln,
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
const jsonPath = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, STATUS_JSON);
|
|
120
|
+
const mdPath = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, STATUS_MD);
|
|
121
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(jsonPath, JSON.stringify(status, null, 2)); } catch {}
|
|
122
|
+
const md = [
|
|
123
|
+
`# Watch status — ${status.ts.slice(11, 19)} UTC`,
|
|
124
|
+
'',
|
|
125
|
+
status.line,
|
|
126
|
+
'',
|
|
127
|
+
];
|
|
128
|
+
if (status.addedTop5.length) {
|
|
129
|
+
md.push('## New findings');
|
|
130
|
+
for (const f of status.addedTop5) {
|
|
131
|
+
md.push(`- **[${(f.severity || '?').toUpperCase()}]** ${f.vuln || f.family || 'finding'} — \`${f.file}:${f.line}\``);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(mdPath, md.join('\n')); } catch {}
|
|
135
|
+
return status;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read the latest watch-status (returns null if none).
|
|
140
|
+
*/
|
|
141
|
+
function readStatus(scanRoot) {
|
|
142
|
+
return _readJsonSafe(path.join(scanRoot, STATE, STATUS_JSON));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Subscribe to FS events and call onChange(absPath, eventType) for each
|
|
147
|
+
* matching event. Debounces bursts. Returns a controller with stop().
|
|
148
|
+
*
|
|
149
|
+
* The actual incremental scan call lives in the caller — this module
|
|
150
|
+
* stays pure for testability.
|
|
151
|
+
*/
|
|
152
|
+
async function watchProject(scanRoot, onChange, opts = {}) {
|
|
153
|
+
if (process.env.AGENTIC_SECURITY_NO_WATCH === '1') return { stop: async () => {}, _disabled: true };
|
|
154
|
+
const recursive = opts.recursive !== false;
|
|
155
|
+
const ac = new AbortController();
|
|
156
|
+
let timer = null;
|
|
157
|
+
const pending = new Set();
|
|
158
|
+
const flush = () => {
|
|
159
|
+
timer = null;
|
|
160
|
+
if (pending.size > MAX_BURST) { pending.clear(); return; }
|
|
161
|
+
const batch = Array.from(pending);
|
|
162
|
+
pending.clear();
|
|
163
|
+
try { onChange(batch); } catch {}
|
|
164
|
+
};
|
|
165
|
+
let stopped = false;
|
|
166
|
+
(async () => {
|
|
167
|
+
try {
|
|
168
|
+
for await (const evt of node_fs_promises__WEBPACK_IMPORTED_MODULE_0__.watch(scanRoot, { recursive, signal: ac.signal })) {
|
|
169
|
+
if (stopped) break;
|
|
170
|
+
const rel = String(evt.filename || '').replace(/\\/g, '/');
|
|
171
|
+
if (!_isScanable(rel)) continue;
|
|
172
|
+
pending.add(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel));
|
|
173
|
+
if (timer) clearTimeout(timer);
|
|
174
|
+
timer = setTimeout(flush, DEBOUNCE_MS);
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (e && e.name !== 'AbortError') {
|
|
178
|
+
// Surface but don't crash — caller decides how to handle.
|
|
179
|
+
try { onChange([], e); } catch {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
return {
|
|
184
|
+
stop: async () => { stopped = true; ac.abort(); if (timer) clearTimeout(timer); },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const _internals = { _isScanable, SCAN_EXT_RE, IGNORE_DIR_RE };
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
/***/ })
|
|
192
|
+
|
|
193
|
+
};
|
package/dist/384.index.js
CHANGED
|
@@ -8,7 +8,7 @@ export const modules = {
|
|
|
8
8
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
9
|
/* harmony export */ scanCredentials: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Sv)
|
|
10
10
|
/* harmony export */ });
|
|
11
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
|
|
11
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3793);
|
|
12
12
|
// Secrets submodule view of the engine — credential + entropy + TODO scanning.
|
|
13
13
|
|
|
14
14
|
|