@clear-capabilities/agentic-security-scanner 0.166.0 → 0.168.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 +91 -0
- package/bin/agentic-security.js +40 -4
- package/dist/2199.index.js +348 -0
- package/dist/3736.index.js +2 -1
- package/dist/3740.index.js +722 -0
- package/dist/4113.index.js +1 -1
- package/dist/4142.index.js +929 -0
- package/dist/5738.index.js +240 -0
- package/dist/6932.index.js +1190 -0
- package/dist/7838.index.js +1 -1
- package/dist/808.index.js +1330 -0
- package/dist/818.index.js +93 -0
- package/dist/9800.index.js +245 -0
- package/dist/9999.index.js +2 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/posture/artifact-registry.js +6 -0
- package/src/posture/fleet/cli.js +103 -0
- package/src/posture/fleet/fleet-graph.js +142 -0
- package/src/posture/fleet/fleet-manifest.js +77 -0
- package/src/posture/proof-fabric/cli.js +115 -0
- package/src/posture/proof-fabric/fix-proof.js +73 -0
- package/src/posture/proof-fabric/oracles.js +101 -0
- package/src/posture/proof-fabric/orchestrator.js +129 -0
- package/src/posture/proof-fabric/reproducer.js +108 -0
- package/src/posture/proof-fabric/runner.js +243 -0
- package/src/posture/proof-fabric/states.js +53 -0
- package/src/posture/proof-fabric/store.js +83 -0
- package/src/posture/rule-compiler/cli.js +141 -0
- package/src/posture/rule-compiler/corpus.js +61 -0
- package/src/posture/rule-compiler/eligibility.js +25 -0
- package/src/posture/rule-compiler/ir.js +87 -0
- package/src/posture/rule-compiler/lifecycle.js +57 -0
- package/src/posture/rule-compiler/mutation.js +60 -0
- package/src/posture/rule-compiler/promotion.js +47 -0
- package/src/posture/rule-compiler/store.js +96 -0
- package/src/sandbox/backend-userspace.js +7 -1
- package/src/sandbox/trust-boundary.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,97 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.168.0 - fleet: multi-repository scan orchestration and cross-repository dependency blast radius
|
|
13
|
+
|
|
14
|
+
A real, honestly-scoped slice of the organization-wide security graph recommendation: a new CLI command group,
|
|
15
|
+
`fleet init|scan|status|resume|query`, built almost entirely on existing machinery rather than a new engine.
|
|
16
|
+
|
|
17
|
+
**The manifest and the scan.** `fleet.yaml` declares an organization and a list of `{id, path}` repositories
|
|
18
|
+
(plus `workers` and `perRepoTimeout`), validated closed-world — an unknown key, a duplicate id, a non-kebab id or
|
|
19
|
+
an unresolvable path refuses the whole manifest. `fleet scan fleet.yaml` drives the EXISTING `posture/fleet.js`
|
|
20
|
+
(`runFleet`) over that list: bounded concurrency, a resumable on-disk checkpoint, per-repository isolation (one
|
|
21
|
+
repo exploding never takes the run down), and an honest rollup (a failed repo is reported failed, never folded
|
|
22
|
+
into "0 findings"). No new scanning engine was written for this.
|
|
23
|
+
|
|
24
|
+
**The new part: a cross-repository dependency graph.** Built purely from each repository's own package manifest
|
|
25
|
+
(`package.json`/`go.mod`/`Cargo.toml`/`requirements.txt`/`pyproject.toml` — name plus dependencies), with no
|
|
26
|
+
deployment configuration required, so a fleet of plain application repositories works immediately. `fleet query
|
|
27
|
+
--depends-on <repo-id>` answers "which repositories depend on this one, directly or transitively, and through
|
|
28
|
+
what path" — a real question about organization-wide blast radius, not a guess.
|
|
29
|
+
|
|
30
|
+
**A real, grounded demonstration, not an asserted-away fixture.** A four-repository synthetic organization
|
|
31
|
+
(frontend, payments-api, shared-auth-lib, payments-worker) where only `shared-auth-lib` carries an actual
|
|
32
|
+
command-injection vulnerability the deterministic engine genuinely finds (the same confirmed-detected shape —
|
|
33
|
+
`req.body.host` into `child_process.exec` — already used in this repository's own fixtures). The blast-radius
|
|
34
|
+
query correctly reports `frontend` and `payments-api` as direct dependents and `payments-worker` as a transitive
|
|
35
|
+
one, through `payments-api`, at the right depth, with the right path.
|
|
36
|
+
|
|
37
|
+
**A bug this process itself caught, twice.** A first draft of the fleet CLI wrapped `runScan()`'s own
|
|
38
|
+
`{ scan, meta }` return value in a SECOND `{ scan }` object; `runFleet`'s internal summarizer reads fields
|
|
39
|
+
defensively rather than throwing on the wrong shape, so every repository silently reported zero findings with no
|
|
40
|
+
error of any kind. Found by running the CLI against the fixture and noticing the finding count was wrong, not by
|
|
41
|
+
the automated test — whose own fixture at the time also happened to use a vulnerability shape the deterministic
|
|
42
|
+
engine does not actually flag (an unlabeled function parameter reaching `exec`, no recognized untrusted source).
|
|
43
|
+
Both are fixed, and the fixture now uses the same confirmed-detected shape the test asserts against.
|
|
44
|
+
|
|
45
|
+
**Still open, and disclosed as such** (`docs/implementation-status/fleet.md`): no Internet-facing/regulated-data
|
|
46
|
+
overlay — the fuller "which Internet-facing app can reach PCI data through this component" question needs the
|
|
47
|
+
existing deployment-boundary graph's trust-zone model layered on top of this dependency graph, not attempted
|
|
48
|
+
here; no natural-language query parser (`--depends-on <repo-id>` is the one supported shape); no SQLite-backed
|
|
49
|
+
storage (reuses the existing flat-JSON fleet checkpoint; `posture/portfolio/backend.js`'s existing storage
|
|
50
|
+
abstraction is the natural place for a SQLite implementation later); no persisted job queue with lease/retry
|
|
51
|
+
(reuses in-process `runFleet` rather than the heavier, operator-only `portfolio/work-units.js` scheduler); matching
|
|
52
|
+
across repositories is by declared package name only, with no ecosystem disambiguation.
|
|
53
|
+
|
|
54
|
+
## 0.167.0 - execution-backed exploit proof, and a self-learning rule compiler that turns one confirmed finding into a deterministic detection rule
|
|
55
|
+
|
|
56
|
+
Two new, independently opt-in capabilities, each off by default until explicitly invoked.
|
|
57
|
+
|
|
58
|
+
**Execution-backed exploit proof (`triage exploit --prove`, `fix --require-proof`).** A new evidence-state ladder
|
|
59
|
+
(`CANDIDATE` → `STATICALLY_CORROBORATED` → `REPRODUCER_GENERATED` → `REPRODUCED` → `EXPLOIT_CONFIRMED` →
|
|
60
|
+
`FIX_REPLAY_BLOCKED` → `FIX_VERIFIED`), separate from severity and confidence, reached only by executing a bounded,
|
|
61
|
+
serialized reproducer inside the existing confined sandbox (`src/sandbox/`) and judging the result with one of nine
|
|
62
|
+
deterministic oracles — never a model. `EXPLOIT_CONFIRMED` requires 3 of 3 reproductions (configurable) plus an
|
|
63
|
+
independent re-run in a fresh environment that receives only the reproducer, the finding id, the oracle and the
|
|
64
|
+
revision, never the reasoning that found it. `fix --apply --require-proof` extends the existing fix-verify
|
|
65
|
+
vocabulary (never a competing definition of "verified"): it refuses to write a patch unless the original exploit is
|
|
66
|
+
confirmed beforehand and the SAME reproducer is demonstrably blocked, in every attempt, afterward. New CLI: `proof
|
|
67
|
+
show|replay|doctor`, `triage exploit <id> --prove`, `triage list --evidence`. Eight fixture classes (SQL injection,
|
|
68
|
+
command injection, path traversal, SSRF, auth bypass, insecure deserialization, unsafe file write, and a real
|
|
69
|
+
AddressSanitizer-backed buffer overflow in C) are tested both directions: vulnerable reaches `EXPLOIT_CONFIRMED`,
|
|
70
|
+
mitigated never does. Guide: `docs/guides/exploit-proof-fabric.md`.
|
|
71
|
+
|
|
72
|
+
Building the fixture corpus surfaced two real, pre-existing defects, each fixed with its own regression test: the
|
|
73
|
+
default (non-capability) confined sandbox profile had no write allowance for `/dev/null`, so any confined command
|
|
74
|
+
redirecting output there (`stdio: 'ignore'`, a common pattern) failed to spawn at all with a silent `EPERM`; and
|
|
75
|
+
the proof runner's own loopback callback listener read the wrong `process.argv` index for a `node -e` invocation,
|
|
76
|
+
so every callback request crashed it (observed by the client as `ECONNRESET`).
|
|
77
|
+
|
|
78
|
+
**Self-learning finding-to-rule compiler (`rules synthesize|evaluate|promote|status|rollback`).** Turns one
|
|
79
|
+
confirmed finding into a reviewed, promoted, deterministic custom rule — compiled into the EXISTING pattern-rule
|
|
80
|
+
engine (`posture/custom-rules.js`), never a second matcher. A candidate rule (its pattern supplied by a model or a
|
|
81
|
+
human; nothing here calls one) must clear a promotion gate — precision ≥95%, recall ≥80%, zero protected-negative
|
|
82
|
+
failures, by default — against an automatically generated corpus that includes deterministic, semantics-preserving
|
|
83
|
+
structural mutations (variable rename, wrap-in-function, indirection) of both the vulnerable and the patched code,
|
|
84
|
+
before an append-only, hash-chained lifecycle ledger lets it advance from `CANDIDATE` to `VALIDATED` to `PROMOTED`.
|
|
85
|
+
A promoted rule is published to the existing `.agentic-security/rules/` directory, so it is live on the very next
|
|
86
|
+
scan with no code change, and gets regression fixtures in the project's own existing `vulnerable`/`clean`
|
|
87
|
+
convention. Demonstrated end to end on a real, grounded miss: the deterministic engine does not recognize
|
|
88
|
+
`db.rawQuery(...)` string concatenation as a SQL sink (verified by a real scan); after promotion, the rule catches
|
|
89
|
+
not just the original finding but an unseen structural sibling — different file, renamed identifiers, extra
|
|
90
|
+
indirection — with zero model calls. Guide: `docs/guides/rule-compiler.md`.
|
|
91
|
+
|
|
92
|
+
A duplicate command name was caught before shipping: the PRD's own example CLI shape collided with the pre-existing
|
|
93
|
+
`rules validate`/`rules lock` (rule-pack override linting and lockfile writing). A first draft added a second,
|
|
94
|
+
competing `case 'rules':` switch arm, which JavaScript resolves to the first match — silently making the original
|
|
95
|
+
two subcommands permanently unreachable. Fixed by folding the new subcommands into the existing dispatcher instead.
|
|
96
|
+
|
|
97
|
+
**Still open.** Neither capability has a model-driven proposal step (both expect the candidate pattern/reproducer
|
|
98
|
+
supplied by the caller); container execution for the proof fabric is detected but not implemented, so execution
|
|
99
|
+
always runs in the existing lower-assurance local sandbox and says so; the rule compiler implements three of the
|
|
100
|
+
PRD's seven listed mutator kinds. Full detail and honest remaining gaps in
|
|
101
|
+
`docs/implementation-status/{exploit-proof-fabric,rule-compiler}.md`.
|
|
102
|
+
|
|
12
103
|
## 0.166.0 - the closure accepts hosted Linux test evidence for tests that skip on a non-Linux host; the shared-portfolio drain test is deterministic
|
|
13
104
|
|
|
14
105
|
**Hosted Linux evidence for skipped tests, kept separate and labelled.** On a macOS host the capabilities suite skips six Linux-namespace tests, and a skipped required test counts as failed, so requirements X-501 to X-508 could never verify there even though the same tests run and pass on the hosted `sandbox-linux` job (which fails on any skip). The controller's verification can now count a locally skipped test only when a commit-bound attestation shows that the `sandbox-linux` check run for the exact commit succeeded and that the named test passed in that job's per-test report (uploaded as the `sandbox-linux-test-report` artifact, bound to the commit, the run and a digest). Anything else (wrong or never-pushed commit, job missing, in progress, failed, cancelled or skipped, test absent or skipped in the report, malformed or forged report, `gh` unavailable, a dirty tree) produces a typed refusal and the skip stays open. Local results are never rewritten: the local skip count is unchanged, covered tests are reported as a separate source (`attested-from-ci: sandbox-linux@<sha>`), and a local failure or abnormal exit never triggers a lookup. It is opt-in per profile (`attestFromCi`, capabilities suite only) and `LOOP_ATTEST_FROM_CI=0` turns it off. This is evidence from a hosted run, not a claim that the local host enforced anything; Linux stays `partially-verified`. The new test file has 60 tests, including every refusal and forged-report cases, and 12 hand-made mutants of the new code (nine caught; two proved equivalent; the remaining three guards are only caught together). A live read against the real hosted run covered all six tests and refused an invented one. Writing the reader exposed one real defect: on pull-request runs the job's commit is the merge ref, so the report is now bound to the pull request head and records the merge ref actually tested as `testedSha`.
|
package/bin/agentic-security.js
CHANGED
|
@@ -1528,6 +1528,7 @@ async function cmdProfile(args) {
|
|
|
1528
1528
|
// /triage list | assign | transition | trend
|
|
1529
1529
|
async function cmdTriage(args) {
|
|
1530
1530
|
const target = path.resolve(args._[args._.length - 1] && !args._[args._.length - 1].startsWith('--') ? args._[args._.length - 1] : '.');
|
|
1531
|
+
const sub = args._[1];
|
|
1531
1532
|
// ollama-offline-prd.md §34 — `triage --explain <finding-id>` is read-only
|
|
1532
1533
|
// narrative generation, not triage's own assign/transition/lock state
|
|
1533
1534
|
// machine, so it doesn't need the pro-tier gate below (and is checked
|
|
@@ -1599,12 +1600,17 @@ async function cmdTriage(args) {
|
|
|
1599
1600
|
}
|
|
1600
1601
|
return 0;
|
|
1601
1602
|
}
|
|
1603
|
+
if (sub === 'exploit' || (sub === 'validate' && args.flags.execution)) {
|
|
1604
|
+
if (sub === 'exploit' && !args.flags.prove) { console.error('triage exploit requires --prove (it executes a reproducer in an isolated environment).'); return 4; }
|
|
1605
|
+
const last = args._[args._.length - 1];
|
|
1606
|
+
const exploitTarget = path.resolve(args.flags.root || (last && !String(last).startsWith('--') && last !== args._[1] && last !== args._[2] ? last : '.'));
|
|
1607
|
+
return (await import('../src/posture/proof-fabric/cli.js')).cmdProve(args, exploitTarget);
|
|
1608
|
+
}
|
|
1602
1609
|
const profile = loadProfile(target);
|
|
1603
1610
|
if (profile.profile !== 'pro') {
|
|
1604
1611
|
console.error('Triage is a pro-mode feature. Run `agentic-security profile set pro` to enable.');
|
|
1605
1612
|
return 4;
|
|
1606
1613
|
}
|
|
1607
|
-
const sub = args._[1];
|
|
1608
1614
|
// Sync first so list reflects the latest scan.
|
|
1609
1615
|
const lastScanPath = statePath(target, 'findings.json');
|
|
1610
1616
|
if (fs.existsSync(lastScanPath)) {
|
|
@@ -1618,10 +1624,16 @@ async function cmdTriage(args) {
|
|
|
1618
1624
|
if (args.flags.assignee) filter.assignee = args.flags.assignee;
|
|
1619
1625
|
if (args.flags.unassigned) filter.unassigned = true;
|
|
1620
1626
|
const items = triage.list(target, filter);
|
|
1621
|
-
|
|
1627
|
+
// --evidence (PRD 1): an optional, read-only column naming the latest stored exploit-proof evidence state for a
|
|
1628
|
+
// finding, when one exists. Decoupled from triage's own open/fixed state machine on purpose — proof evidence and
|
|
1629
|
+
// triage workflow state answer different questions and must not be conflated.
|
|
1630
|
+
const showEvidence = !!args.flags.evidence;
|
|
1631
|
+
const latestProofFn = showEvidence ? (await import('../src/posture/proof-fabric/store.js')).latestProof : null;
|
|
1632
|
+
const hdr = ['ID', 'Severity', 'State', 'Assignee', 'File:Line', 'Vuln', ...(showEvidence ? ['Evidence'] : [])].join(' ');
|
|
1622
1633
|
console.log(hdr);
|
|
1623
1634
|
console.log('─'.repeat(80));
|
|
1624
1635
|
for (const t of items.slice(0, 50)) {
|
|
1636
|
+
const evidence = showEvidence ? (latestProofFn(target, t.id)?.record?.state ?? 'none') : null;
|
|
1625
1637
|
console.log([
|
|
1626
1638
|
t.id.slice(0, 16),
|
|
1627
1639
|
(t.severity || '').padEnd(8),
|
|
@@ -1629,6 +1641,7 @@ async function cmdTriage(args) {
|
|
|
1629
1641
|
(t.assignee || '—').padEnd(20),
|
|
1630
1642
|
`${t.file}:${t.line}`.padEnd(40),
|
|
1631
1643
|
t.vuln,
|
|
1644
|
+
...(showEvidence ? [evidence] : []),
|
|
1632
1645
|
].join(' '));
|
|
1633
1646
|
}
|
|
1634
1647
|
return 0;
|
|
@@ -1740,8 +1753,14 @@ async function cmdOrgScan(args) {
|
|
|
1740
1753
|
|
|
1741
1754
|
// /rules validate | rules lock
|
|
1742
1755
|
async function cmdRules(args) {
|
|
1743
|
-
const target = path.resolve(args._[2] || '.');
|
|
1744
1756
|
const sub = args._[1];
|
|
1757
|
+
// Self-learning finding-to-rule compiler (PRD 3): synthesize/evaluate/promote/status/rollback a DETECTION rule
|
|
1758
|
+
// from one confirmed finding — distinct from validate/lock below (rules.yml override linting and pack locking).
|
|
1759
|
+
// Dispatches to its own module rather than inlining here, same convention as `triage exploit`/`proof`.
|
|
1760
|
+
if (sub === 'synthesize' || sub === 'evaluate' || sub === 'promote' || sub === 'status' || sub === 'rollback') {
|
|
1761
|
+
return (await import('../src/posture/rule-compiler/cli.js')).cmdRules(args, path.resolve(args.flags.root || '.'));
|
|
1762
|
+
}
|
|
1763
|
+
const target = path.resolve(args._[2] || '.');
|
|
1745
1764
|
if (sub === 'validate') {
|
|
1746
1765
|
const r = validateOverrides(target);
|
|
1747
1766
|
if (r.ok) { console.log('✓ rules.yml is valid'); return 0; }
|
|
@@ -1755,7 +1774,7 @@ async function cmdRules(args) {
|
|
|
1755
1774
|
console.log(` scanner: ${lock.scannerVersion} rulePackHash: ${lock.rulePackHash}`);
|
|
1756
1775
|
return 0;
|
|
1757
1776
|
}
|
|
1758
|
-
console.error('rules validate | rules lock'); return 4;
|
|
1777
|
+
console.error('rules validate | rules lock | rules synthesize <finding-id> --spec <file.json> | rules evaluate|promote|status|rollback <rule-id>'); return 4;
|
|
1759
1778
|
}
|
|
1760
1779
|
|
|
1761
1780
|
// `agentic-security secure [--launch]` — smart router. One command picks the
|
|
@@ -3617,6 +3636,21 @@ async function cmdFix(args) {
|
|
|
3617
3636
|
const fixMeta = (approvedBy || approvalReason || patchAuthor)
|
|
3618
3637
|
? { approval: { approvedBy: approvedBy || '', reason: approvalReason || '' }, ...(patchAuthor ? { author: patchAuthor } : {}) }
|
|
3619
3638
|
: null;
|
|
3639
|
+
// --require-proof: the patch is applied ONLY if the original exploit is confirmed beforehand and demonstrably blocked
|
|
3640
|
+
// afterwards (posture/proof-fabric/fix-proof.js). Missing reproducer, unavailable environment or an incomplete replay all refuse.
|
|
3641
|
+
if (args.flags['require-proof']) {
|
|
3642
|
+
const { loadReproducerFor } = await import('../src/posture/proof-fabric/cli.js');
|
|
3643
|
+
const { verifyFixWithProof, formatFixProof } = await import('../src/posture/proof-fabric/fix-proof.js');
|
|
3644
|
+
const { verifyFix } = await import('../src/posture/fix-verify.js');
|
|
3645
|
+
const loaded = loadReproducerFor(scanRoot, f.id, args.flags.reproducer);
|
|
3646
|
+
if (!loaded) { console.error(`--require-proof: no reproducer for ${f.id}. Refusing to apply; nothing was executed. Supply --reproducer <file>.`); return 4; }
|
|
3647
|
+
const files = { [f.file]: newContent };
|
|
3648
|
+
const staticVerification = await verifyFix({ scanRoot, originalFindingStableId: f.stableId || null, files, recordMetrics: false });
|
|
3649
|
+
const siblings = (last.findings || []).filter((x) => x.id !== f.id && x.file !== f.file && (x.vuln === f.vuln || x.cwe === f.cwe)).map((x) => ({ id: x.id, file: x.file, line: x.line }));
|
|
3650
|
+
const fp = await verifyFixWithProof({ finding: f, reproducer: loaded.reproducer, files, scanRoot, staticVerification, siblings });
|
|
3651
|
+
console.log(formatFixProof(fp));
|
|
3652
|
+
if (!fp.verified) { console.error('\nRefusing to apply: the fix is not proof-verified (--require-proof).'); return 4; }
|
|
3653
|
+
}
|
|
3620
3654
|
let result = await applyVerifiedFix({
|
|
3621
3655
|
scanRoot,
|
|
3622
3656
|
finding: { file: f.file, id: f.id, stableId: f.stableId || null, ruleId: f.cwe || f.title, vuln: f.vuln || f.title },
|
|
@@ -7077,9 +7111,11 @@ async function main() {
|
|
|
7077
7111
|
case 'undo': process.exit(await cmdUndo(args));
|
|
7078
7112
|
case 'accept': process.exit(await cmdAccept(args));
|
|
7079
7113
|
case 'profile': process.exit(await cmdProfile(args));
|
|
7114
|
+
case 'proof': process.exit(await (await import('../src/posture/proof-fabric/cli.js')).cmdProof(args, path.resolve(args.flags.root || '.')));
|
|
7080
7115
|
case 'triage': process.exit(await cmdTriage(args));
|
|
7081
7116
|
case 'org-scan': process.exit(await cmdOrgScan(args));
|
|
7082
7117
|
case 'rules': process.exit(await cmdRules(args));
|
|
7118
|
+
case 'fleet': process.exit(await (await import('../src/posture/fleet/cli.js')).cmdFleet(args, path.resolve(args.flags.root || '.')));
|
|
7083
7119
|
case 'rule': process.exit(await cmdRule(args));
|
|
7084
7120
|
case 'tickets': process.exit(await cmdTickets(args));
|
|
7085
7121
|
case 'secure': process.exit(await cmdSecure(args));
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
export const id = 2199;
|
|
2
|
+
export const ids = [2199];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 25517:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ $: () => (/* binding */ ORACLE_TYPES),
|
|
10
|
+
/* harmony export */ U: () => (/* binding */ evaluateOracle)
|
|
11
|
+
/* harmony export */ });
|
|
12
|
+
// Deterministic security oracles. A model may SELECT an oracle and propose its parameters; it may never decide that the
|
|
13
|
+
// oracle succeeded. Each oracle is a pure function of an Observation gathered by the TRUSTED runner (never by the model):
|
|
14
|
+
//
|
|
15
|
+
// Observation = {
|
|
16
|
+
// exitCode, signal, timedOut, stdout, stderr,
|
|
17
|
+
// canaries: { <name>: { exists, digestBefore, digestAfter } } // checked by the runner on disk, after the run
|
|
18
|
+
// callbacks: [{ nonce }] // hits recorded by the runner's loopback listener
|
|
19
|
+
// nonce // per-run random token the target cannot predict
|
|
20
|
+
// }
|
|
21
|
+
//
|
|
22
|
+
// An oracle returns { fired, reason }. `fired:false` with `inconclusive:true` means the observation could not decide
|
|
23
|
+
// (for example a timeout); it is never treated as "not exploitable".
|
|
24
|
+
const ORACLE_TYPES = Object.freeze([
|
|
25
|
+
'crash_sanitizer', 'command_execution', 'unauthorized_http_access', 'unauthorized_db_read', 'database_mutation',
|
|
26
|
+
'filesystem_mutation', 'outbound_callback', 'authentication_bypass', 'arbitrary_assertion',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SANITIZER_RE = /(AddressSanitizer|UndefinedBehaviorSanitizer|stack-buffer-overflow|heap-buffer-overflow|SEGV on unknown address|stack smashing detected|buffer overflow detected)/i;
|
|
30
|
+
const CRASH_SIGNALS = new Set(['SIGSEGV', 'SIGABRT', 'SIGBUS', 'SIGILL', 'SIGFPE']);
|
|
31
|
+
|
|
32
|
+
const out = (fired, reason, extra = {}) => ({ fired, reason, ...extra });
|
|
33
|
+
const inconclusive = (reason) => ({ fired: false, inconclusive: true, reason });
|
|
34
|
+
const has = (obs, text) => typeof text === 'string' && text.length > 0 && `${obs.stdout || ''}${obs.stderr || ''}`.includes(text);
|
|
35
|
+
|
|
36
|
+
const IMPL = {
|
|
37
|
+
crash_sanitizer(obs) {
|
|
38
|
+
if (SANITIZER_RE.test(obs.stderr || '')) return out(true, 'sanitizer or runtime memory-safety report on stderr');
|
|
39
|
+
if (obs.signal && CRASH_SIGNALS.has(obs.signal)) return out(true, `process terminated by ${obs.signal}`);
|
|
40
|
+
if (obs.timedOut) return inconclusive('run timed out before a crash could be judged');
|
|
41
|
+
return out(false, 'no crash signal and no sanitizer report');
|
|
42
|
+
},
|
|
43
|
+
// The injected payload asks the shell/interpreter to create a file named after the per-run nonce inside the
|
|
44
|
+
// watched canary directory. The target cannot create it unless attacker data reached a command.
|
|
45
|
+
command_execution(obs, p) {
|
|
46
|
+
const c = obs.canaries?.[p.canary || 'command'];
|
|
47
|
+
if (!c) return inconclusive('command canary was not armed by the runner');
|
|
48
|
+
if (c.exists && !c.existedBefore) return out(true, 'nonce-named canary file created by an injected command');
|
|
49
|
+
if (obs.timedOut) return inconclusive('run timed out before the canary could be judged');
|
|
50
|
+
return out(false, 'canary file not created');
|
|
51
|
+
},
|
|
52
|
+
// A secret planted behind authorization (never shown to the exploit client). Seeing the nonce-bearing secret in the
|
|
53
|
+
// client's output while the actor held no credential is the violation.
|
|
54
|
+
unauthorized_http_access(obs, p) {
|
|
55
|
+
const secret = obs.secrets?.[p.secret || 'http'];
|
|
56
|
+
if (!secret) return inconclusive('protected secret was not armed by the runner');
|
|
57
|
+
if (has(obs, secret)) return out(true, 'protected resource content returned to an unauthenticated actor');
|
|
58
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
59
|
+
return out(false, 'protected content not observed');
|
|
60
|
+
},
|
|
61
|
+
unauthorized_db_read(obs, p) {
|
|
62
|
+
const secret = obs.secrets?.[p.secret || 'db'];
|
|
63
|
+
if (!secret) return inconclusive('database canary row was not armed by the runner');
|
|
64
|
+
if (has(obs, secret)) return out(true, 'row the actor may not read was returned');
|
|
65
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
66
|
+
return out(false, 'canary row value not observed');
|
|
67
|
+
},
|
|
68
|
+
database_mutation(obs, p) {
|
|
69
|
+
const c = obs.canaries?.[p.canary || 'db'];
|
|
70
|
+
if (!c) return inconclusive('database canary was not armed by the runner');
|
|
71
|
+
if (c.digestBefore && c.digestAfter && c.digestBefore !== c.digestAfter) return out(true, 'protected database state changed');
|
|
72
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
73
|
+
return out(false, 'protected database state unchanged');
|
|
74
|
+
},
|
|
75
|
+
filesystem_mutation(obs, p) {
|
|
76
|
+
const c = obs.canaries?.[p.canary || 'fs'];
|
|
77
|
+
if (!c) return inconclusive('filesystem canary was not armed by the runner');
|
|
78
|
+
if ((c.exists && !c.existedBefore) || (c.digestBefore && c.digestAfter && c.digestBefore !== c.digestAfter)) return out(true, 'file outside the permitted area was created or modified');
|
|
79
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
80
|
+
return out(false, 'protected path unchanged');
|
|
81
|
+
},
|
|
82
|
+
outbound_callback(obs) {
|
|
83
|
+
if (obs.callbackUnavailable) return { fired: false, unsupported: true, reason: 'loopback callback listener is not available on this backend' };
|
|
84
|
+
const hit = (obs.callbacks || []).some((c) => c && c.nonce && c.nonce === obs.nonce);
|
|
85
|
+
if (hit) return out(true, 'nonce-bearing request reached the runner-controlled listener');
|
|
86
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
87
|
+
return out(false, 'no nonce-bearing callback received');
|
|
88
|
+
},
|
|
89
|
+
authentication_bypass(obs, p) {
|
|
90
|
+
const secret = obs.secrets?.[p.secret || 'auth'];
|
|
91
|
+
if (!secret) return inconclusive('protected secret was not armed by the runner');
|
|
92
|
+
if (has(obs, secret)) return out(true, 'privileged content returned without valid credentials');
|
|
93
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
94
|
+
return out(false, 'privileged content not observed without credentials');
|
|
95
|
+
},
|
|
96
|
+
// Declarative only: no code, no regex supplied by a model. Operators are a closed set.
|
|
97
|
+
arbitrary_assertion(obs, p) {
|
|
98
|
+
const a = p.assert;
|
|
99
|
+
if (!a || typeof a !== 'object') return inconclusive('assertion missing');
|
|
100
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
101
|
+
if ('exitCode' in a) return a.exitCode === obs.exitCode ? out(true, `exit code was ${obs.exitCode}`) : out(false, `exit code was ${obs.exitCode}`);
|
|
102
|
+
if ('stdoutIncludes' in a) return has({ stdout: obs.stdout }, String(a.stdoutIncludes)) ? out(true, 'stdout contained the asserted text') : out(false, 'stdout did not contain the asserted text');
|
|
103
|
+
if ('stderrIncludes' in a) return has({ stderr: obs.stderr }, String(a.stderrIncludes)) ? out(true, 'stderr contained the asserted text') : out(false, 'stderr did not contain the asserted text');
|
|
104
|
+
return inconclusive('unsupported assertion operator');
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function evaluateOracle(spec, observation) {
|
|
109
|
+
if (!spec || !ORACLE_TYPES.includes(spec.type)) return { fired: false, unsupported: true, reason: `unknown oracle type: ${String(spec?.type)}` };
|
|
110
|
+
if (!observation || typeof observation !== 'object') return inconclusive('no observation');
|
|
111
|
+
try { return IMPL[spec.type](observation, spec.params || {}); } catch (e) { return inconclusive(`oracle error: ${e.message}`); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
/***/ }),
|
|
116
|
+
|
|
117
|
+
/***/ 37055:
|
|
118
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
119
|
+
|
|
120
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
121
|
+
/* harmony export */ IW: () => (/* binding */ verifierView),
|
|
122
|
+
/* harmony export */ V2: () => (/* binding */ substitute),
|
|
123
|
+
/* harmony export */ Yj: () => (/* binding */ LANGUAGES),
|
|
124
|
+
/* harmony export */ dw: () => (/* binding */ canonicalize),
|
|
125
|
+
/* harmony export */ mC: () => (/* binding */ validateReproducer),
|
|
126
|
+
/* harmony export */ sc: () => (/* binding */ sha256),
|
|
127
|
+
/* harmony export */ we: () => (/* binding */ reproducerDigest)
|
|
128
|
+
/* harmony export */ });
|
|
129
|
+
/* unused harmony exports MAX_FILES, MAX_FILE_BYTES, isSafeRelPath */
|
|
130
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
|
|
131
|
+
/* harmony import */ var _oracles_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25517);
|
|
132
|
+
// Serialized reproducer: the ONLY thing an independent verifier receives (besides revision, finding id and oracle).
|
|
133
|
+
// It carries no reasoning, no model output and no prior results. Everything per-run (nonce, secrets, canary paths,
|
|
134
|
+
// callback URL) is a placeholder substituted fresh by the trusted runner, so a replay can never reuse a stale value
|
|
135
|
+
// and a target cannot learn the values in advance.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
const REPRODUCER_SCHEMA = 'agentic-security/reproducer';
|
|
140
|
+
const REPRODUCER_VERSION = 1;
|
|
141
|
+
const LANGUAGES = Object.freeze(['javascript', 'typescript', 'python', 'java', 'c', 'cpp', 'csharp']);
|
|
142
|
+
const MAX_FILES = 64;
|
|
143
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
144
|
+
const MAX_TOTAL_BYTES = 1024 * 1024;
|
|
145
|
+
const PLACEHOLDER_RE = /\{\{([A-Z_]+)(?::([a-z0-9_-]{1,32}))?\}\}/g;
|
|
146
|
+
const ALLOWED_PLACEHOLDERS = new Set(['NONCE', 'SECRET', 'CANARY', 'CALLBACK_URL', 'CALLBACK_PORT', 'ROOT', 'NODE', 'PYTHON', 'JAVA', 'JAVAC', 'CC']);
|
|
147
|
+
|
|
148
|
+
function isSafeRelPath(rel) {
|
|
149
|
+
if (typeof rel !== 'string' || !rel || rel.length > 200) return false;
|
|
150
|
+
if (rel.startsWith('/') || rel.includes('\0') || rel.includes('\\')) return false;
|
|
151
|
+
if (['canary', 'tmp'].includes(rel.split('/')[0])) return false;
|
|
152
|
+
return rel.split('/').every((seg) => seg && seg !== '.' && seg !== '..' && !seg.startsWith('__oracle_'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Canonical JSON (sorted keys) so identical reproducers hash identically regardless of key order. */
|
|
156
|
+
function canonicalize(v) {
|
|
157
|
+
if (Array.isArray(v)) return `[${v.map(canonicalize).join(',')}]`;
|
|
158
|
+
if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
|
|
159
|
+
return JSON.stringify(v);
|
|
160
|
+
}
|
|
161
|
+
const sha256 = (s) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash)('sha256').update(s).digest('hex');
|
|
162
|
+
const reproducerDigest = (r) => sha256(canonicalize(r));
|
|
163
|
+
|
|
164
|
+
function validateReproducer(r) {
|
|
165
|
+
const errs = [];
|
|
166
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) return ['reproducer must be an object'];
|
|
167
|
+
if (r.schema !== REPRODUCER_SCHEMA) errs.push('unknown schema');
|
|
168
|
+
if (r.version !== REPRODUCER_VERSION) errs.push('unsupported reproducer version');
|
|
169
|
+
if (typeof r.findingId !== 'string' || !r.findingId || r.findingId.length > 200) errs.push('findingId required');
|
|
170
|
+
if (!LANGUAGES.includes(r.lang)) errs.push(`unsupported language: ${String(r.lang)}`);
|
|
171
|
+
if (!r.oracle || !_oracles_js__WEBPACK_IMPORTED_MODULE_1__/* .ORACLE_TYPES */ .$.includes(r.oracle.type)) errs.push(`unknown oracle: ${String(r.oracle?.type)}`);
|
|
172
|
+
const files = r.files ?? {};
|
|
173
|
+
if (typeof files !== 'object' || Array.isArray(files)) errs.push('files must be an object');
|
|
174
|
+
else {
|
|
175
|
+
const names = Object.keys(files);
|
|
176
|
+
if (names.length > MAX_FILES) errs.push(`file count must be at most ${MAX_FILES}`);
|
|
177
|
+
let total = 0;
|
|
178
|
+
for (const n of names) {
|
|
179
|
+
if (!isSafeRelPath(n)) { errs.push(`unsafe file path: ${n}`); continue; }
|
|
180
|
+
if (typeof files[n] !== 'string') { errs.push(`file ${n} must be a string`); continue; }
|
|
181
|
+
const b = Buffer.byteLength(files[n]);
|
|
182
|
+
total += b;
|
|
183
|
+
if (b > MAX_FILE_BYTES) errs.push(`file ${n} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
184
|
+
}
|
|
185
|
+
if (total > MAX_TOTAL_BYTES) errs.push('reproducer exceeds total size limit');
|
|
186
|
+
}
|
|
187
|
+
const sourceNames = r.sources && typeof r.sources === 'object' && !Array.isArray(r.sources) ? Object.keys(r.sources) : [];
|
|
188
|
+
if (Object.keys(files).length + sourceNames.length === 0) errs.push('reproducer must declare at least one file or source');
|
|
189
|
+
for (const key of ['build', 'run']) {
|
|
190
|
+
const a = r[key];
|
|
191
|
+
if (a == null && key === 'build') continue;
|
|
192
|
+
if (!Array.isArray(a) || !a.length || !a.every((x) => typeof x === 'string' && x.length < 2000 && !x.includes('\0'))) errs.push(`${key} must be a non-empty argv array of strings (no shell string)`);
|
|
193
|
+
}
|
|
194
|
+
for (const text of [...Object.values(files || {}), ...(r.build || []), ...(r.run || [])]) {
|
|
195
|
+
if (typeof text !== 'string') continue;
|
|
196
|
+
for (const m of text.matchAll(PLACEHOLDER_RE)) if (!ALLOWED_PLACEHOLDERS.has(m[1])) errs.push(`unknown placeholder {{${m[1]}}}`);
|
|
197
|
+
}
|
|
198
|
+
if (r.watch != null) {
|
|
199
|
+
if (typeof r.watch !== 'object' || Array.isArray(r.watch)) errs.push('watch must be an object');
|
|
200
|
+
else for (const [k, v] of Object.entries(r.watch)) if (!/^[a-z0-9_-]{1,32}$/.test(k) || !isSafeRelPath(v)) errs.push(`bad watch entry: ${k}`);
|
|
201
|
+
}
|
|
202
|
+
if (r.env != null) {
|
|
203
|
+
if (typeof r.env !== 'object' || Array.isArray(r.env)) errs.push('env must be an object');
|
|
204
|
+
else for (const [k, v] of Object.entries(r.env)) {
|
|
205
|
+
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(k)) errs.push(`bad env var name: ${k}`);
|
|
206
|
+
if (typeof v !== 'string' || v.length > 4096) errs.push(`env value for ${k} must be a short string`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (r.sources != null) {
|
|
210
|
+
if (typeof r.sources !== 'object' || Array.isArray(r.sources)) errs.push('sources must be an object');
|
|
211
|
+
else for (const [dest, src] of Object.entries(r.sources)) if (!isSafeRelPath(dest) || !isSafeRelPath(src)) errs.push(`bad sources entry: ${dest}`);
|
|
212
|
+
}
|
|
213
|
+
if (r.network != null && !['none', 'callback'].includes(r.network)) errs.push('network must be none or callback');
|
|
214
|
+
return errs;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The view handed to an independent verifier: exactly the reproducer, plus identifiers. Nothing else. */
|
|
218
|
+
function verifierView({ revision, reproducer, findingId, oracle }) {
|
|
219
|
+
return Object.freeze({ revision: revision ?? null, findingId, oracle, reproducer: structuredClone(reproducer) });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function substitute(text, ctx) {
|
|
223
|
+
return text.replace(PLACEHOLDER_RE, (whole, name, arg) => {
|
|
224
|
+
switch (name) {
|
|
225
|
+
case 'NONCE': return ctx.nonce;
|
|
226
|
+
case 'SECRET': return ctx.secret(arg || 'default');
|
|
227
|
+
case 'CANARY': return ctx.canary(arg || 'default');
|
|
228
|
+
case 'CALLBACK_URL': return ctx.callbackUrl ?? whole;
|
|
229
|
+
case 'CALLBACK_PORT': return String(ctx.callbackPort ?? '');
|
|
230
|
+
case 'ROOT': return ctx.root;
|
|
231
|
+
case 'NODE': return ctx.tools.node;
|
|
232
|
+
case 'PYTHON': return ctx.tools.python ?? whole;
|
|
233
|
+
case 'JAVA': return ctx.tools.java ?? whole;
|
|
234
|
+
case 'JAVAC': return ctx.tools.javac ?? whole;
|
|
235
|
+
case 'CC': return ctx.tools.cc ?? whole;
|
|
236
|
+
default: return whole;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
/***/ }),
|
|
243
|
+
|
|
244
|
+
/***/ 62199:
|
|
245
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
246
|
+
|
|
247
|
+
__webpack_require__.r(__webpack_exports__);
|
|
248
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
249
|
+
/* harmony export */ findingKey: () => (/* binding */ findingKey),
|
|
250
|
+
/* harmony export */ latestProof: () => (/* binding */ latestProof),
|
|
251
|
+
/* harmony export */ listProofs: () => (/* binding */ listProofs),
|
|
252
|
+
/* harmony export */ saveProof: () => (/* binding */ saveProof),
|
|
253
|
+
/* harmony export */ scrubDeep: () => (/* binding */ scrubDeep)
|
|
254
|
+
/* harmony export */ });
|
|
255
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
256
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
|
|
257
|
+
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
|
|
258
|
+
/* harmony import */ var _encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39984);
|
|
259
|
+
/* harmony import */ var _llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(65388);
|
|
260
|
+
/* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(37055);
|
|
261
|
+
// Proof artifact store: `.agentic-security/proofs/<findingKey>/<recordDigest>.json`.
|
|
262
|
+
//
|
|
263
|
+
// Reuses the existing state machinery rather than adding another: `safeWriteState` (refuses unsafe dirs and symlinks),
|
|
264
|
+
// `maybeEncryptForWrite` / `maybeDecryptForRead` (the `proofs` artifact is registered `confidential`), retention
|
|
265
|
+
// (evidence class) and legal holds (a hold on `proofs` blocks purge). Nothing persisted contains a plaintext credential:
|
|
266
|
+
// records are passed through a recursive secret filter first, and the runner never returns its per-run nonce or secrets.
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
const PROOF_RECORD_SCHEMA = 'agentic-security/proof-record';
|
|
275
|
+
const PROOFS_DIR = 'proofs';
|
|
276
|
+
const MAX_RECORD_BYTES = 512 * 1024;
|
|
277
|
+
|
|
278
|
+
const findingKey = (id) => `${String(id).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80)}-${(0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)(String(id)).slice(0, 8)}`;
|
|
279
|
+
|
|
280
|
+
// The shared redactor catches key=value shapes; bare tokens need their own pass, so persisted evidence cannot carry one.
|
|
281
|
+
const BARE_SECRET_RES = [
|
|
282
|
+
/\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/g, /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
283
|
+
/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g, /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
284
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, /\bSECRET-[0-9a-f]{24}-[a-z0-9_-]+\b/g,
|
|
285
|
+
];
|
|
286
|
+
function scrubString(text) {
|
|
287
|
+
let t = String(text);
|
|
288
|
+
try { t = (0,_llm_validator_redact_js__WEBPACK_IMPORTED_MODULE_4__/* .redactSecrets */ .f)(t).text; } catch { return '[unredactable]'; }
|
|
289
|
+
for (const re of BARE_SECRET_RES) t = t.replace(re, '[REDACTED-SECRET]');
|
|
290
|
+
return t;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Deep-copy `v` with every string passed through the secret redactor. */
|
|
294
|
+
function scrubDeep(v) {
|
|
295
|
+
if (typeof v === 'string') return scrubString(v);
|
|
296
|
+
if (Array.isArray(v)) return v.map(scrubDeep);
|
|
297
|
+
if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, scrubDeep(x)]));
|
|
298
|
+
return v;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function recordDigest(record) {
|
|
302
|
+
const { recordDigest: _omit, ...rest } = record;
|
|
303
|
+
return (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)((0,_reproducer_js__WEBPACK_IMPORTED_MODULE_5__/* .canonicalize */ .dw)(rest));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function saveProof(scanRoot, record) {
|
|
307
|
+
const safe = scrubDeep(record);
|
|
308
|
+
safe.schema = PROOF_RECORD_SCHEMA;
|
|
309
|
+
safe.recordDigest = recordDigest(safe);
|
|
310
|
+
const text = JSON.stringify(safe, null, 2);
|
|
311
|
+
if (Buffer.byteLength(text) > MAX_RECORD_BYTES) return { ok: false, reason: 'proof record exceeds the size limit' };
|
|
312
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .ensureStateDir */ .xH)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
313
|
+
const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(scanRoot, PROOFS_DIR, text);
|
|
314
|
+
if (!enc.ok) return { ok: false, reason: enc.reason };
|
|
315
|
+
const file = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(record.findingId), `${safe.recordDigest}.json`);
|
|
316
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .safeWriteState */ .Ep)(file, enc.content)) return { ok: false, reason: 'state write refused' };
|
|
317
|
+
return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readRecord(file) {
|
|
321
|
+
const text = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__.maybeDecryptForRead)(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8'));
|
|
322
|
+
const rec = JSON.parse(text);
|
|
323
|
+
// A record whose content no longer matches its own digest was modified after it was written.
|
|
324
|
+
if (rec.recordDigest !== recordDigest(rec)) return { tampered: true, record: rec };
|
|
325
|
+
return { tampered: false, record: rec };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Newest-first records for a finding. Tampered records are returned flagged, never silently dropped or trusted. */
|
|
329
|
+
function listProofs(scanRoot, findingId) {
|
|
330
|
+
const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, PROOFS_DIR, findingKey(findingId));
|
|
331
|
+
let names = [];
|
|
332
|
+
try { names = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir).filter((n) => n.endsWith('.json')); } catch { return []; }
|
|
333
|
+
const out = [];
|
|
334
|
+
for (const n of names) {
|
|
335
|
+
const f = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, n);
|
|
336
|
+
try { const { tampered, record } = readRecord(f); out.push({ file: f, tampered, record, mtime: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(f).mtimeMs }); } catch { out.push({ file: f, tampered: true, record: null, mtime: 0 }); }
|
|
337
|
+
}
|
|
338
|
+
return out.sort((a, b) => b.mtime - a.mtime);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function latestProof(scanRoot, findingId) {
|
|
342
|
+
return listProofs(scanRoot, findingId).find((p) => p.record) ?? null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
/***/ })
|
|
347
|
+
|
|
348
|
+
};
|
package/dist/3736.index.js
CHANGED
|
@@ -10,10 +10,11 @@ export const modules = {
|
|
|
10
10
|
/* harmony export */ ensurePolicyKeyPair: () => (/* binding */ ensurePolicyKeyPair),
|
|
11
11
|
/* harmony export */ loadPolicyBundles: () => (/* binding */ loadPolicyBundles),
|
|
12
12
|
/* harmony export */ loadPolicyPublicKey: () => (/* binding */ loadPolicyPublicKey),
|
|
13
|
+
/* harmony export */ rE: () => (/* binding */ computePolicyDrift),
|
|
13
14
|
/* harmony export */ resolveEffectivePolicy: () => (/* binding */ resolveEffectivePolicy),
|
|
14
15
|
/* harmony export */ signPolicyBundle: () => (/* binding */ signPolicyBundle)
|
|
15
16
|
/* harmony export */ });
|
|
16
|
-
/* unused harmony exports POLICY_BUNDLE_SCHEMA, SCOPES, canonicalPolicyBytes, verifyPolicyBundle
|
|
17
|
+
/* unused harmony exports POLICY_BUNDLE_SCHEMA, SCOPES, canonicalPolicyBytes, verifyPolicyBundle */
|
|
17
18
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
18
19
|
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
|
|
19
20
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
|