@clear-capabilities/agentic-security-scanner 0.172.0 → 0.173.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 +29 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1336.index.js +101 -101
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7405.index.js +23 -6
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +338 -48
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +28 -28
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +31 -6
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/adapters/haskell-hie.js +28 -1
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/canonical.js +25 -0
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +16 -10
- package/src/context-intelligence/config.js +4 -1
- package/src/context-intelligence/controls.js +108 -10
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/entrypoints.js +2 -2
- package/src/context-intelligence/evaluation/index.js +11 -4
- package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +10 -4
- package/src/context-intelligence/review.js +129 -16
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +272 -20
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/util/string-memo.js +38 -0
package/dist/2199.index.js
CHANGED
|
@@ -2,245 +2,6 @@ export const id = 2199;
|
|
|
2
2
|
export const ids = [2199];
|
|
3
3
|
export const modules = {
|
|
4
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
5
|
/***/ 62199:
|
|
245
6
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
246
7
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export const id = 2223;
|
|
2
|
+
export const ids = [2223];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 82223:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ detectVendoredLibraries: () => (/* binding */ detectVendoredLibraries)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
// Vendored / copied library detection via version-string fingerprinting.
|
|
12
|
+
//
|
|
13
|
+
// Detects library code copied into src/ that bypasses SCA. Uses version
|
|
14
|
+
// string patterns (_.VERSION, jQuery.fn.jquery, etc.) and characteristic
|
|
15
|
+
// function signatures to identify vendored libraries.
|
|
16
|
+
|
|
17
|
+
const VERSION_FINGERPRINTS = [
|
|
18
|
+
{ pkg: 'lodash', ecosystem: 'npm', patterns: [
|
|
19
|
+
{ re: /\b(?:lodash|_)\.VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
20
|
+
{ re: /\b__lodash_hash_undefined__\b/, version: null },
|
|
21
|
+
]},
|
|
22
|
+
{ pkg: 'jquery', ecosystem: 'npm', patterns: [
|
|
23
|
+
{ re: /jQuery\.fn\.jquery\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
24
|
+
{ re: /\bjQuery\.fn\.init\b/, version: null },
|
|
25
|
+
]},
|
|
26
|
+
{ pkg: 'underscore', ecosystem: 'npm', patterns: [
|
|
27
|
+
{ re: /\b_\.VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
28
|
+
]},
|
|
29
|
+
{ pkg: 'moment', ecosystem: 'npm', patterns: [
|
|
30
|
+
{ re: /\bmoment\.version\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
31
|
+
{ re: /\bmoment\.(?:utc|parseZone|duration|locale)\b/, version: null },
|
|
32
|
+
]},
|
|
33
|
+
{ pkg: 'handlebars', ecosystem: 'npm', patterns: [
|
|
34
|
+
{ re: /\bHandlebars\.VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
35
|
+
]},
|
|
36
|
+
{ pkg: 'backbone', ecosystem: 'npm', patterns: [
|
|
37
|
+
{ re: /\bBackbone\.VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
38
|
+
]},
|
|
39
|
+
{ pkg: 'angular', ecosystem: 'npm', patterns: [
|
|
40
|
+
{ re: /\bangular\.version\s*=\s*\{[^}]*full\s*:\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
41
|
+
]},
|
|
42
|
+
{ pkg: 'vue', ecosystem: 'npm', patterns: [
|
|
43
|
+
{ re: /\bVue\.version\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
44
|
+
]},
|
|
45
|
+
{ pkg: 'react', ecosystem: 'npm', patterns: [
|
|
46
|
+
{ re: /\bReactVersion\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
47
|
+
]},
|
|
48
|
+
{ pkg: 'dompurify', ecosystem: 'npm', patterns: [
|
|
49
|
+
{ re: /\bDOMPurify\.version\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
50
|
+
]},
|
|
51
|
+
{ pkg: 'marked', ecosystem: 'npm', patterns: [
|
|
52
|
+
{ re: /\bmarked\.(?:defaults|setOptions|use|parse)\b[\s\S]{0,200}version\s*[:=]\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
53
|
+
]},
|
|
54
|
+
{ pkg: 'axios', ecosystem: 'npm', patterns: [
|
|
55
|
+
{ re: /\baxios\.VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
56
|
+
]},
|
|
57
|
+
{ pkg: 'socket.io-client', ecosystem: 'npm', patterns: [
|
|
58
|
+
{ re: /\bio\.protocol\s*=\s*(\d+)/, version: null },
|
|
59
|
+
]},
|
|
60
|
+
{ pkg: 'highlight.js', ecosystem: 'npm', patterns: [
|
|
61
|
+
{ re: /\bhljs\.versionString\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
62
|
+
]},
|
|
63
|
+
{ pkg: 'chart.js', ecosystem: 'npm', patterns: [
|
|
64
|
+
{ re: /\bChart\.version\s*=\s*['"](\d+\.\d+\.\d+)['"]/, versionGroup: 1 },
|
|
65
|
+
]},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const SKIP_DIRS = /(?:^|[/\\])(?:node_modules|vendor|dist|build|\.next|__pycache__|\.git)[/\\]/;
|
|
69
|
+
|
|
70
|
+
function detectVendoredLibraries(fileContents) {
|
|
71
|
+
if (!fileContents || typeof fileContents !== 'object') return [];
|
|
72
|
+
const detected = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
|
|
75
|
+
for (const [fp, content] of Object.entries(fileContents)) {
|
|
76
|
+
if (!content || typeof content !== 'string') continue;
|
|
77
|
+
if (SKIP_DIRS.test(fp)) continue;
|
|
78
|
+
if (content.length < 500) continue;
|
|
79
|
+
|
|
80
|
+
for (const lib of VERSION_FINGERPRINTS) {
|
|
81
|
+
for (const pat of lib.patterns) {
|
|
82
|
+
const m = content.match(pat.re);
|
|
83
|
+
if (!m) continue;
|
|
84
|
+
const version = pat.versionGroup ? m[pat.versionGroup] : null;
|
|
85
|
+
const key = `${lib.pkg}:${fp}`;
|
|
86
|
+
if (seen.has(key)) continue;
|
|
87
|
+
seen.add(key);
|
|
88
|
+
detected.push({
|
|
89
|
+
name: lib.pkg,
|
|
90
|
+
version: version || 'unknown',
|
|
91
|
+
ecosystem: lib.ecosystem,
|
|
92
|
+
file: fp,
|
|
93
|
+
scope: 'vendored',
|
|
94
|
+
isVendored: true,
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Pass 2: Function-body structural matching for minified/forked copies
|
|
101
|
+
const FUNCTION_BODY_SIGS = [
|
|
102
|
+
{ pkg: 'lodash', ecosystem: 'npm', fn: 'merge', paramMin: 1,
|
|
103
|
+
bodyContains: ['assignValue', 'baseFor', 'isObject', 'baseMerge'] },
|
|
104
|
+
{ pkg: 'lodash', ecosystem: 'npm', fn: 'template', paramMin: 1,
|
|
105
|
+
bodyContains: ['sourceURL', 'interpolate', 'evaluate', 'escape'] },
|
|
106
|
+
{ pkg: 'lodash', ecosystem: 'npm', fn: 'defaultsDeep', paramMin: 1,
|
|
107
|
+
bodyContains: ['baseMerge', 'isMergeableObject', 'customDefaultsMerge'] },
|
|
108
|
+
{ pkg: 'jquery', ecosystem: 'npm', fn: 'ajax', paramMin: 1,
|
|
109
|
+
bodyContains: ['XMLHttpRequest', 'ajaxSettings', 'crossDomain', 'responseFields'] },
|
|
110
|
+
{ pkg: 'handlebars', ecosystem: 'npm', fn: 'compile', paramMin: 1,
|
|
111
|
+
bodyContains: ['templateSpec', 'container', 'invokePartial', 'blockParams'] },
|
|
112
|
+
{ pkg: 'marked', ecosystem: 'npm', fn: 'parse', paramMin: 1,
|
|
113
|
+
bodyContains: ['Lexer', 'Parser', 'blockTokens', 'walkTokens'] },
|
|
114
|
+
{ pkg: 'ejs', ecosystem: 'npm', fn: 'render', paramMin: 1,
|
|
115
|
+
bodyContains: ['includeFile', 'resolveInclude', 'rethrow', 'escapeFn'] },
|
|
116
|
+
{ pkg: 'moment', ecosystem: 'npm', fn: 'format', paramMin: 0,
|
|
117
|
+
bodyContains: ['formatMoment', 'expandFormat', 'makeFormatFunction', 'localFormattingTokens'] },
|
|
118
|
+
{ pkg: 'underscore', ecosystem: 'npm', fn: 'template', paramMin: 1,
|
|
119
|
+
bodyContains: ['interpolate', 'evaluate', 'escape', 'templateSettings'] },
|
|
120
|
+
{ pkg: 'minimist', ecosystem: 'npm', fn: 'parse', paramMin: 1,
|
|
121
|
+
bodyContains: ['boolean', 'alias', 'default', 'stopEarly', 'unknown'] },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
for (const [fp, content] of Object.entries(fileContents)) {
|
|
125
|
+
if (!content || typeof content !== 'string') continue;
|
|
126
|
+
if (SKIP_DIRS.test(fp)) continue;
|
|
127
|
+
if (!/\.(?:js|mjs|cjs)$/i.test(fp)) continue;
|
|
128
|
+
if (content.length < 200 || content.length > 500_000) continue;
|
|
129
|
+
|
|
130
|
+
for (const sig of FUNCTION_BODY_SIGS) {
|
|
131
|
+
const key = `${sig.pkg}:${fp}`;
|
|
132
|
+
if (seen.has(key)) continue;
|
|
133
|
+
const fnRe = new RegExp(`(?:function\\s+${sig.fn}|(?:const|let|var)\\s+${sig.fn}\\s*=|${sig.fn}\\s*[:=]\\s*function)\\s*\\(`, 'g');
|
|
134
|
+
const m = fnRe.exec(content);
|
|
135
|
+
if (!m) continue;
|
|
136
|
+
const bodyWindow = content.slice(m.index, m.index + 2000);
|
|
137
|
+
const matchCount = sig.bodyContains.filter(kw => bodyWindow.includes(kw)).length;
|
|
138
|
+
if (matchCount < Math.ceil(sig.bodyContains.length * 0.6)) continue;
|
|
139
|
+
seen.add(key);
|
|
140
|
+
detected.push({
|
|
141
|
+
name: sig.pkg,
|
|
142
|
+
version: 'unknown',
|
|
143
|
+
ecosystem: sig.ecosystem,
|
|
144
|
+
file: fp,
|
|
145
|
+
scope: 'vendored',
|
|
146
|
+
isVendored: true,
|
|
147
|
+
_detectionMethod: 'function-body-signature',
|
|
148
|
+
_matchedKeywords: matchCount,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return detected;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
/***/ })
|
|
158
|
+
|
|
159
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const id = 2530;
|
|
2
|
+
export const ids = [2530];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 92530:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ verifyReleasePackage: () => (/* binding */ verifyReleasePackage)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony export MINIMUM_KINDS */
|
|
12
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
|
|
13
|
+
// The verifier for a signed release assurance package (E7-03). Used by `scripts/verify-release-package.mjs` (a command-line wrapper) and by `release-view`.
|
|
14
|
+
//
|
|
15
|
+
// This file imports nothing but Node built-ins. It does not use the producer's code, so a defect in the producer cannot make it agree with itself. What it
|
|
16
|
+
// checks, each reported under its own code:
|
|
17
|
+
// FORMAT known schema, supported major version, no unknown top-level keys
|
|
18
|
+
// SIGNATURE Ed25519 over the domain label plus the canonical JSON of everything but the signature, verified with the key the CALLER supplies; the key the
|
|
19
|
+
// package names must be the same key (a package cannot vouch for itself, and a signature lifted from another package does not verify)
|
|
20
|
+
// SNAPSHOT the snapshot digest matches its fields
|
|
21
|
+
// EVIDENCE every record matches the digest in its reference; every reference matches its own digest; every reference is bound to this snapshot
|
|
22
|
+
// LINEAGE every dependency is present, there is no cycle, and the stated order is the dependency order
|
|
23
|
+
// MANDATORY every kind in the verifier's own minimum, in the caller's --require list, and in the package's own list is present
|
|
24
|
+
// COMPLETE the package may not claim a complete scan over a scan health that is not complete
|
|
25
|
+
// FINDINGS each finding's proof tier and exploitability flag equal what the finding itself says
|
|
26
|
+
// ASSERTIONS every cited record is in the package, and a "supported" statement cites at least one
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const MINIMUM_KINDS = Object.freeze(['scan-health', 'finding']);
|
|
30
|
+
const SCHEMA = 'agentic-security/release-package';
|
|
31
|
+
const DOMAIN = `${SCHEMA}@1\n`;
|
|
32
|
+
const TOP_KEYS = new Set(['schema', 'schemaVersion', 'snapshot', 'evidence', 'lineageOrder', 'requiredKinds', 'findings', 'completeness', 'policy', 'assertions', 'proves', 'doesNotProve', 'issuance', 'signature']);
|
|
33
|
+
const ROLES = new Set(['finding', 'completeness', 'context', 'control', 'inventory']);
|
|
34
|
+
|
|
35
|
+
const canon = (v) => {
|
|
36
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v ?? null);
|
|
37
|
+
if (Array.isArray(v)) return `[${v.map(canon).join(',')}]`;
|
|
38
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
|
|
39
|
+
};
|
|
40
|
+
const digest = (v) => `sha256:${node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(canon(v)).digest('hex')}`;
|
|
41
|
+
const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
42
|
+
|
|
43
|
+
function verifyReleasePackage(pkg, publicKeyPem, { require: extraRequired = [] } = {}) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const fail = (code, message) => errors.push({ code, message });
|
|
46
|
+
if (!isObj(pkg)) return { ok: false, errors: [{ code: 'FORMAT', message: 'the package is not an object' }] };
|
|
47
|
+
if (pkg.schema !== SCHEMA) fail('FORMAT', `unrecognised schema ${JSON.stringify(pkg.schema)}`);
|
|
48
|
+
const major = Number(String(pkg.schemaVersion).split('.')[0]);
|
|
49
|
+
if (major !== 1) fail('FORMAT', `unsupported version ${JSON.stringify(pkg.schemaVersion)}`);
|
|
50
|
+
for (const k of Object.keys(pkg)) if (!TOP_KEYS.has(k)) fail('FORMAT', `unrecognised top-level key "${k}", which the signature does not vouch for`);
|
|
51
|
+
if (errors.length) return { ok: false, errors };
|
|
52
|
+
|
|
53
|
+
// ---- SIGNATURE
|
|
54
|
+
const sig = pkg.signature;
|
|
55
|
+
let publicKey = null;
|
|
56
|
+
if (!publicKeyPem) fail('SIGNATURE', 'no public key was supplied');
|
|
57
|
+
else { try { publicKey = node_crypto__WEBPACK_IMPORTED_MODULE_0__.createPublicKey(publicKeyPem); } catch (e) { fail('SIGNATURE', `the public key is unusable: ${e.message}`); } }
|
|
58
|
+
if (!isObj(sig) || sig.algorithm !== 'ed25519' || typeof sig.value !== 'string') fail('SIGNATURE', 'the package is unsigned or uses an unsupported algorithm');
|
|
59
|
+
else if (publicKey) {
|
|
60
|
+
const { signature, ...rest } = pkg;
|
|
61
|
+
let ok = false;
|
|
62
|
+
try { ok = node_crypto__WEBPACK_IMPORTED_MODULE_0__.verify(null, Buffer.from(DOMAIN + canon(rest), 'utf8'), publicKey, Buffer.from(sig.value, 'base64')); } catch (e) { fail('SIGNATURE', `verification error: ${e.message}`); }
|
|
63
|
+
if (!ok && !errors.some((e) => e.code === 'SIGNATURE')) fail('SIGNATURE', 'the signature does not match the package contents: something changed after signing, or it was signed by another key');
|
|
64
|
+
const fp = node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(publicKey.export({ type: 'spki', format: 'der' })).digest('hex');
|
|
65
|
+
const named = pkg.issuance && pkg.issuance.issuer && pkg.issuance.issuer.keyFingerprint;
|
|
66
|
+
if (named !== fp) fail('SIGNATURE', 'the key the package names is not the key supplied for verification');
|
|
67
|
+
if (!pkg.issuance || pkg.issuance.independentlyCertified !== false) fail('SIGNATURE', 'a self-issued package may not claim independent certification');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- SNAPSHOT
|
|
71
|
+
const snap = pkg.snapshot;
|
|
72
|
+
if (!isObj(snap)) fail('SNAPSHOT', 'there is no snapshot');
|
|
73
|
+
else {
|
|
74
|
+
const { digest: d, immutable, ...body } = snap;
|
|
75
|
+
if (d !== digest(body)) fail('SNAPSHOT', 'the snapshot does not match its digest');
|
|
76
|
+
if (immutable !== (!!body.commit && body.dirty === false)) fail('SNAPSHOT', 'the snapshot\'s immutability flag is not what its fields imply');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- EVIDENCE
|
|
80
|
+
const entries = Array.isArray(pkg.evidence) ? pkg.evidence : [];
|
|
81
|
+
if (!Array.isArray(pkg.evidence)) fail('EVIDENCE', 'evidence is not a list');
|
|
82
|
+
const byDigest = new Map();
|
|
83
|
+
for (const [i, e] of entries.entries()) {
|
|
84
|
+
if (!isObj(e) || !isObj(e.ref) || !('payload' in e)) { fail('EVIDENCE', `entry ${i} is malformed`); continue; }
|
|
85
|
+
if (!ROLES.has(e.role)) fail('EVIDENCE', `entry ${i} has an unknown role ${JSON.stringify(e.role)}`);
|
|
86
|
+
const { digest: rd, findingProvenanceDigest, otherProvenanceDigest, ...rb } = e.ref;
|
|
87
|
+
if (rd !== digest(rb)) fail('EVIDENCE', `the reference of entry ${i} (${e.ref.kind}) does not match its own digest`);
|
|
88
|
+
if (e.ref.payloadDigest !== digest(e.payload)) fail('EVIDENCE', `the ${e.ref.kind} record in entry ${i} does not match the digest in its reference`);
|
|
89
|
+
if (isObj(snap) && e.ref.snapshotDigest !== snap.digest) fail('EVIDENCE', `the ${e.ref.kind} record in entry ${i} describes a different snapshot`);
|
|
90
|
+
if (byDigest.has(e.ref.digest)) fail('EVIDENCE', `entry ${i} duplicates another reference`);
|
|
91
|
+
byDigest.set(e.ref.digest, e);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- LINEAGE
|
|
95
|
+
for (const e of byDigest.values()) for (const dep of e.ref.restsOn || []) if (!byDigest.has(dep)) fail('LINEAGE', `${e.ref.kind} rests on a record that is not in the package (${String(dep).slice(0, 19)}...)`);
|
|
96
|
+
const state = new Map(); const order = [];
|
|
97
|
+
const visit = (d, trail) => {
|
|
98
|
+
if (state.get(d) === 2) return; if (state.get(d) === 1) { fail('LINEAGE', `cycle through ${trail.concat(d).map((x) => x.slice(7, 15)).join(' -> ')}`); return; }
|
|
99
|
+
state.set(d, 1);
|
|
100
|
+
for (const dep of (byDigest.get(d) || { ref: {} }).ref.restsOn || []) if (byDigest.has(dep)) visit(dep, trail.concat(d));
|
|
101
|
+
state.set(d, 2); order.push(d);
|
|
102
|
+
};
|
|
103
|
+
for (const d of [...byDigest.keys()].sort()) visit(d, []);
|
|
104
|
+
if (JSON.stringify(order) !== JSON.stringify(pkg.lineageOrder)) fail('LINEAGE', 'the stated order is not the dependency order');
|
|
105
|
+
if (JSON.stringify(entries.map((e) => e && e.ref && e.ref.digest)) !== JSON.stringify(pkg.lineageOrder)) fail('LINEAGE', 'the evidence list is not in the stated order');
|
|
106
|
+
|
|
107
|
+
// ---- MANDATORY
|
|
108
|
+
const kinds = new Set([...byDigest.values()].map((e) => e.ref.kind));
|
|
109
|
+
const needed = new Set([...MINIMUM_KINDS, ...extraRequired, ...(Array.isArray(pkg.requiredKinds) ? pkg.requiredKinds : [])]);
|
|
110
|
+
for (const k of MINIMUM_KINDS) if (!(Array.isArray(pkg.requiredKinds) && pkg.requiredKinds.includes(k))) fail('MANDATORY', `the package does not declare "${k}" as required`);
|
|
111
|
+
for (const k of [...needed].sort()) if (!kinds.has(k)) fail('MANDATORY', `required evidence is missing: ${k}`);
|
|
112
|
+
|
|
113
|
+
// ---- COMPLETE
|
|
114
|
+
const healthEntry = [...byDigest.values()].find((e) => e.ref.kind === 'scan-health');
|
|
115
|
+
const c = pkg.completeness;
|
|
116
|
+
if (!isObj(c)) fail('COMPLETE', 'there is no completeness statement');
|
|
117
|
+
else if (healthEntry) {
|
|
118
|
+
const hs = healthEntry.payload && healthEntry.payload.status;
|
|
119
|
+
if (c.scanStatus !== hs) fail('COMPLETE', `the package states scan status ${JSON.stringify(c.scanStatus)} but the scan health says ${JSON.stringify(hs)}`);
|
|
120
|
+
if (c.verdict === 'complete' && hs !== 'complete') fail('COMPLETE', 'the package claims a complete scan over a scan health that is not complete');
|
|
121
|
+
if (c.verdict !== 'complete' && hs === 'complete') fail('COMPLETE', 'the package calls an unfinished scan complete or the reverse: the verdict disagrees with the scan health');
|
|
122
|
+
if (JSON.stringify(c.conditions) !== JSON.stringify(Array.isArray(healthEntry.payload.conditions) ? healthEntry.payload.conditions : [])) fail('COMPLETE', 'the stated conditions differ from the scan health\'s own');
|
|
123
|
+
if (c.source !== healthEntry.ref.digest) fail('COMPLETE', 'the completeness statement cites a different scan health');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- FINDINGS
|
|
127
|
+
const findingEntries = [...byDigest.values()].filter((e) => e.ref.kind === 'finding');
|
|
128
|
+
const listed = new Map((Array.isArray(pkg.findings) ? pkg.findings : []).map((f) => [f.findingRef, f]));
|
|
129
|
+
if (listed.size !== findingEntries.length) fail('FINDINGS', `the package lists ${listed.size} findings but contains ${findingEntries.length}`);
|
|
130
|
+
for (const e of findingEntries) {
|
|
131
|
+
const f = listed.get(e.ref.digest);
|
|
132
|
+
if (!f) { fail('FINDINGS', `finding ${e.ref.payloadId} is in the package but not in its findings summary`); continue; }
|
|
133
|
+
const tier = e.payload && typeof e.payload.proofTier === 'string' ? e.payload.proofTier : 'unproven';
|
|
134
|
+
if (f.proofTier !== tier) fail('FINDINGS', `finding ${e.ref.payloadId}: the summary says proof tier ${JSON.stringify(f.proofTier)} but the finding says ${JSON.stringify(tier)}`);
|
|
135
|
+
if (f.exploitabilityProven !== (tier === 'execution-proven')) fail('FINDINGS', `finding ${e.ref.payloadId}: exploitability is stated as proven without an execution proof`);
|
|
136
|
+
if (f.integrity !== 'unmodified-since-signing') fail('FINDINGS', `finding ${e.ref.payloadId}: unexpected integrity statement`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- ASSERTIONS
|
|
140
|
+
for (const a of Array.isArray(pkg.assertions) ? pkg.assertions : []) {
|
|
141
|
+
for (const d of a.evidence || []) if (!byDigest.has(d)) fail('ASSERTIONS', `assertion ${a.id} cites a record that is not in the package`);
|
|
142
|
+
if (a.status === 'supported' && !(a.evidence || []).length) fail('ASSERTIONS', `assertion ${a.id} is "supported" but cites nothing`);
|
|
143
|
+
if (!['supported', 'unsupported', 'unknown'].includes(a.status)) fail('ASSERTIONS', `assertion ${a.id} has an unknown status`);
|
|
144
|
+
}
|
|
145
|
+
return { ok: errors.length === 0, errors, issuer: pkg.issuance && pkg.issuance.issuer, completeness: isObj(c) ? c.verdict : null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/***/ })
|
|
151
|
+
|
|
152
|
+
};
|