@clear-capabilities/agentic-security-scanner 0.172.1 → 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 +15 -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/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/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 +135 -11
- 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 +27 -27
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +28 -3
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +2 -0
- package/src/context-intelligence/config.js +3 -1
- package/src/context-intelligence/controls.js +49 -1
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- 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 +2 -2
- package/src/context-intelligence/review.js +68 -4
- 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
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
export const id = 9292;
|
|
2
|
+
export const ids = [9292,119];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 69292:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ ImageArchiveError: () => (/* binding */ ImageArchiveError),
|
|
10
|
+
/* harmony export */ t: () => (/* binding */ parseImageArchive)
|
|
11
|
+
/* harmony export */ });
|
|
12
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
|
|
13
|
+
/* harmony import */ var _safe_tar_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30119);
|
|
14
|
+
// Parses a Docker-save ("docker-archive") or OCI-layout ("oci-archive") tar
|
|
15
|
+
// into one normalized shape, without executing or daemon-mounting the image
|
|
16
|
+
// (PRD 5 Safety Requirements: "OCI inspection should not require executing
|
|
17
|
+
// the image"). Built entirely on `safe-tar.js` — every byte read here passes
|
|
18
|
+
// through the same entry-count/size/path/decompression guards.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ImageArchiveError extends Error {
|
|
23
|
+
constructor(message, code) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'ImageArchiveError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeJsonParse(buf, what) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(buf.toString('utf8'));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new ImageArchiveError(`${what} is not valid JSON: ${err.message}`, 'malformed_manifest');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readOuterEntries(buf, limits) {
|
|
39
|
+
const outer = (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .decompressIfGzip */ .zr)(buf, limits);
|
|
40
|
+
const byName = new Map();
|
|
41
|
+
const unsafeEntries = [];
|
|
42
|
+
for (const e of (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .readTarEntries */ .ET)(outer, limits)) {
|
|
43
|
+
if (e.unsafe) {
|
|
44
|
+
unsafeEntries.push({ name: e.name });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
byName.set(e.name, e);
|
|
48
|
+
}
|
|
49
|
+
return { byName, unsafeEntries };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractConfigShape(configObj) {
|
|
53
|
+
const cfg = (configObj && configObj.config) || {};
|
|
54
|
+
return {
|
|
55
|
+
architecture: configObj.architecture || null,
|
|
56
|
+
os: configObj.os || null,
|
|
57
|
+
labels: (cfg.Labels && typeof cfg.Labels === 'object') ? { ...cfg.Labels } : {},
|
|
58
|
+
entrypoint: Array.isArray(cfg.Entrypoint) ? [...cfg.Entrypoint] : null,
|
|
59
|
+
cmd: Array.isArray(cfg.Cmd) ? [...cfg.Cmd] : null,
|
|
60
|
+
history: Array.isArray(configObj.history)
|
|
61
|
+
? configObj.history.map((h) => ({ createdBy: h.created_by || null, created: h.created || null, emptyLayer: !!h.empty_layer }))
|
|
62
|
+
: [],
|
|
63
|
+
rootfsDiffIds: (configObj.rootfs && Array.isArray(configObj.rootfs.diff_ids)) ? [...configObj.rootfs.diff_ids] : [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseDockerArchive(byName, outerUnsafe, limits) {
|
|
68
|
+
const manifestEntry = byName.get('manifest.json');
|
|
69
|
+
const manifestArr = safeJsonParse(manifestEntry.data, 'manifest.json');
|
|
70
|
+
if (!Array.isArray(manifestArr) || manifestArr.length === 0) {
|
|
71
|
+
throw new ImageArchiveError('manifest.json contains no images', 'empty_manifest');
|
|
72
|
+
}
|
|
73
|
+
if (manifestArr.length > 1) {
|
|
74
|
+
throw new ImageArchiveError(`manifest.json describes ${manifestArr.length} images; image scan expects exactly one`, 'multi_image_archive');
|
|
75
|
+
}
|
|
76
|
+
const m = manifestArr[0];
|
|
77
|
+
if (typeof m.Config !== 'string' || !Array.isArray(m.Layers)) {
|
|
78
|
+
throw new ImageArchiveError('manifest.json entry is missing Config or Layers', 'malformed_manifest');
|
|
79
|
+
}
|
|
80
|
+
const configEntry = byName.get(m.Config);
|
|
81
|
+
if (!configEntry) throw new ImageArchiveError(`manifest.json references missing config "${m.Config}"`, 'missing_blob');
|
|
82
|
+
const configObj = safeJsonParse(configEntry.data, m.Config);
|
|
83
|
+
const configDigest = `sha256:${node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(configEntry.data).digest('hex')}`;
|
|
84
|
+
|
|
85
|
+
const layers = [];
|
|
86
|
+
for (let i = 0; i < m.Layers.length; i++) {
|
|
87
|
+
const layerPath = m.Layers[i];
|
|
88
|
+
const entry = byName.get(layerPath);
|
|
89
|
+
if (!entry) throw new ImageArchiveError(`manifest.json references missing layer "${layerPath}"`, 'missing_blob');
|
|
90
|
+
const tar = (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .decompressIfGzip */ .zr)(entry.data, limits);
|
|
91
|
+
layers.push({ index: i, path: layerPath, diffId: configObj?.rootfs?.diff_ids?.[i] || null, tar, sizeBytes: entry.size });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
format: 'docker-archive',
|
|
96
|
+
digest: configDigest,
|
|
97
|
+
digestKind: 'config',
|
|
98
|
+
repoTags: Array.isArray(m.RepoTags) ? m.RepoTags : [],
|
|
99
|
+
config: extractConfigShape(configObj),
|
|
100
|
+
layers,
|
|
101
|
+
unsafeEntries: outerUnsafe,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseOciArchive(byName, outerUnsafe, limits) {
|
|
106
|
+
const indexObj = safeJsonParse(byName.get('index.json').data, 'index.json');
|
|
107
|
+
const manifests = Array.isArray(indexObj.manifests) ? indexObj.manifests : [];
|
|
108
|
+
if (manifests.length === 0) throw new ImageArchiveError('index.json lists no manifests', 'empty_manifest');
|
|
109
|
+
if (manifests.length > 1) {
|
|
110
|
+
throw new ImageArchiveError(`index.json describes ${manifests.length} manifests; image scan expects exactly one`, 'multi_image_archive');
|
|
111
|
+
}
|
|
112
|
+
const manifestDigest = manifests[0].digest;
|
|
113
|
+
if (typeof manifestDigest !== 'string' || !manifestDigest.startsWith('sha256:')) {
|
|
114
|
+
throw new ImageArchiveError('index.json manifest entry has no sha256 digest', 'malformed_manifest');
|
|
115
|
+
}
|
|
116
|
+
const manifestBlobPath = `blobs/sha256/${manifestDigest.slice('sha256:'.length)}`;
|
|
117
|
+
const manifestEntry = byName.get(manifestBlobPath);
|
|
118
|
+
if (!manifestEntry) throw new ImageArchiveError(`index.json references missing manifest blob "${manifestBlobPath}"`, 'missing_blob');
|
|
119
|
+
const manifestObj = safeJsonParse(manifestEntry.data, manifestBlobPath);
|
|
120
|
+
|
|
121
|
+
const configDigest = manifestObj.config && manifestObj.config.digest;
|
|
122
|
+
if (typeof configDigest !== 'string' || !configDigest.startsWith('sha256:')) {
|
|
123
|
+
throw new ImageArchiveError('image manifest has no config digest', 'malformed_manifest');
|
|
124
|
+
}
|
|
125
|
+
const configBlobPath = `blobs/sha256/${configDigest.slice('sha256:'.length)}`;
|
|
126
|
+
const configEntry = byName.get(configBlobPath);
|
|
127
|
+
if (!configEntry) throw new ImageArchiveError(`manifest references missing config blob "${configBlobPath}"`, 'missing_blob');
|
|
128
|
+
const configObj = safeJsonParse(configEntry.data, configBlobPath);
|
|
129
|
+
|
|
130
|
+
const layerDescs = Array.isArray(manifestObj.layers) ? manifestObj.layers : [];
|
|
131
|
+
const layers = [];
|
|
132
|
+
for (let i = 0; i < layerDescs.length; i++) {
|
|
133
|
+
const d = layerDescs[i];
|
|
134
|
+
if (typeof d.digest !== 'string' || !d.digest.startsWith('sha256:')) {
|
|
135
|
+
throw new ImageArchiveError(`layer ${i} has no sha256 digest`, 'malformed_manifest');
|
|
136
|
+
}
|
|
137
|
+
const blobPath = `blobs/sha256/${d.digest.slice('sha256:'.length)}`;
|
|
138
|
+
const entry = byName.get(blobPath);
|
|
139
|
+
if (!entry) throw new ImageArchiveError(`manifest references missing layer blob "${blobPath}"`, 'missing_blob');
|
|
140
|
+
const tar = (0,_safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .decompressIfGzip */ .zr)(entry.data, limits);
|
|
141
|
+
layers.push({ index: i, path: blobPath, diffId: d.digest, mediaType: d.mediaType || null, tar, sizeBytes: entry.size });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
format: 'oci-archive',
|
|
146
|
+
digest: manifestDigest,
|
|
147
|
+
digestKind: 'manifest',
|
|
148
|
+
repoTags: [],
|
|
149
|
+
config: extractConfigShape(configObj),
|
|
150
|
+
layers,
|
|
151
|
+
unsafeEntries: outerUnsafe,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Parses an in-memory tar buffer (already read from disk/stdin by the
|
|
157
|
+
* caller — this module never touches the filesystem or network) as either a
|
|
158
|
+
* `docker save`-shaped archive or an OCI-layout archive. Throws
|
|
159
|
+
* ImageArchiveError for anything it cannot recognize or whose manifest
|
|
160
|
+
* references a blob that isn't present — never guesses or substitutes a
|
|
161
|
+
* default.
|
|
162
|
+
*/
|
|
163
|
+
function parseImageArchive(buf, limits = _safe_tar_js__WEBPACK_IMPORTED_MODULE_1__/* .DEFAULT_LIMITS */ .dR) {
|
|
164
|
+
if (!Buffer.isBuffer(buf)) throw new ImageArchiveError('image archive input must be a Buffer', 'bad_input');
|
|
165
|
+
const { byName, unsafeEntries } = readOuterEntries(buf, limits);
|
|
166
|
+
if (byName.has('manifest.json')) return parseDockerArchive(byName, unsafeEntries, limits);
|
|
167
|
+
if (byName.has('index.json') && byName.has('oci-layout')) return parseOciArchive(byName, unsafeEntries, limits);
|
|
168
|
+
throw new ImageArchiveError(
|
|
169
|
+
'archive is neither a docker-archive (manifest.json) nor an oci-archive (oci-layout + index.json)',
|
|
170
|
+
'unrecognized_format',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
/***/ }),
|
|
176
|
+
|
|
177
|
+
/***/ 30119:
|
|
178
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
179
|
+
|
|
180
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
181
|
+
/* harmony export */ ET: () => (/* binding */ readTarEntries),
|
|
182
|
+
/* harmony export */ Jn: () => (/* binding */ whiteoutTarget),
|
|
183
|
+
/* harmony export */ TarSafetyError: () => (/* binding */ TarSafetyError),
|
|
184
|
+
/* harmony export */ dR: () => (/* binding */ DEFAULT_LIMITS),
|
|
185
|
+
/* harmony export */ l0: () => (/* binding */ isWhiteoutName),
|
|
186
|
+
/* harmony export */ zr: () => (/* binding */ decompressIfGzip)
|
|
187
|
+
/* harmony export */ });
|
|
188
|
+
/* unused harmony export isSafeEntryName */
|
|
189
|
+
/* harmony import */ var node_zlib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(38522);
|
|
190
|
+
// Hand-rolled, hardened POSIX/GNU tar reader for OCI/Docker image archives.
|
|
191
|
+
//
|
|
192
|
+
// Why hand-rolled instead of a dependency: this repo has no tar library (see
|
|
193
|
+
// `src/artifact/CLAUDE.md`), and PRD 5's own Safety Requirements ask for exact
|
|
194
|
+
// control over the failure modes a generic tar library treats as implementation
|
|
195
|
+
// detail: malicious entry paths, decompression bombs, giant layers, oversized
|
|
196
|
+
// archives. Rolling it ourselves means every one of those limits is a named,
|
|
197
|
+
// tested constant in this file rather than a library default we'd have to audit.
|
|
198
|
+
//
|
|
199
|
+
// Scope: ustar + GNU longname ('L' typeflag) + base-256 size fields. PAX extended
|
|
200
|
+
// headers ('x'/'g') and GNU longlink ('K') are structurally skipped (their data
|
|
201
|
+
// block is consumed so offsets stay correct, but their content is not applied) —
|
|
202
|
+
// a disclosed limitation, not a silent one. Real docker-save/oci-archive output
|
|
203
|
+
// from Go's archive/tar for images in this size/complexity class essentially
|
|
204
|
+
// never needs them; synthetic fixtures in this codebase never emit them either.
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
const BLOCK = 512;
|
|
208
|
+
|
|
209
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
210
|
+
maxEntries: 200_000,
|
|
211
|
+
maxEntrySize: 512 * 1024 * 1024, // 512MB per tar entry
|
|
212
|
+
maxTotalSize: 4 * 1024 * 1024 * 1024, // 4GB per archive (sum of entry sizes)
|
|
213
|
+
maxGunzipOutput: 4 * 1024 * 1024 * 1024, // decompression-bomb guard
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
class TarSafetyError extends Error {
|
|
217
|
+
constructor(message, code) {
|
|
218
|
+
super(message);
|
|
219
|
+
this.name = 'TarSafetyError';
|
|
220
|
+
this.code = code;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseSizeField(buf) {
|
|
225
|
+
if (buf[0] & 0x80) {
|
|
226
|
+
// GNU base-256 extension for sizes that don't fit in octal-ASCII.
|
|
227
|
+
let n = 0n;
|
|
228
|
+
for (let i = 1; i < buf.length; i++) n = (n << 8n) | BigInt(buf[i]);
|
|
229
|
+
return Number(n);
|
|
230
|
+
}
|
|
231
|
+
const s = buf.toString('ascii').replace(/\0.*$/, '').trim();
|
|
232
|
+
if (!s) return 0;
|
|
233
|
+
const n = parseInt(s, 8);
|
|
234
|
+
return Number.isFinite(n) ? n : 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cstr(buf) {
|
|
238
|
+
return buf.toString('utf8').replace(/\0.*$/, '');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A tar entry name is unsafe when it would escape a virtual extraction root:
|
|
242
|
+
// absolute paths, `..` segments, embedded NULs, or an unreasonable length.
|
|
243
|
+
// Because this module never writes to a real filesystem path (every caller
|
|
244
|
+
// keeps extracted content in an in-memory Map keyed by this string), the
|
|
245
|
+
// classic zip-slip *write* vulnerability cannot occur here — but a hostile
|
|
246
|
+
// name must still never be silently treated as an ordinary path by downstream
|
|
247
|
+
// manifest/package-DB matching, so unsafe entries are flagged, never dropped
|
|
248
|
+
// silently and never accepted as-is.
|
|
249
|
+
function isSafeEntryName(name) {
|
|
250
|
+
if (typeof name !== 'string' || name.length === 0 || name.length > 4096) return false;
|
|
251
|
+
if (name.includes('\0')) return false;
|
|
252
|
+
const norm = name.replace(/\\/g, '/');
|
|
253
|
+
if (norm.startsWith('/')) return false;
|
|
254
|
+
if (norm.split('/').some((p) => p === '..')) return false;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isWhiteoutName(name) {
|
|
259
|
+
return (name.split('/').pop() || '').startsWith('.wh.');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Resolves a whiteout entry name to what it deletes. `.wh..wh..opq` marks a
|
|
263
|
+
// directory as "opaque" (everything from earlier layers under it is hidden,
|
|
264
|
+
// not individually deleted) — reported distinctly so callers don't conflate
|
|
265
|
+
// it with a single-file deletion.
|
|
266
|
+
function whiteoutTarget(name) {
|
|
267
|
+
const parts = name.split('/');
|
|
268
|
+
const base = parts.pop();
|
|
269
|
+
if (base === '.wh..wh..opq') return { opaque: true, dir: parts.join('/') };
|
|
270
|
+
return { opaque: false, path: [...parts, base.slice(4)].join('/') };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Decompresses a gzip member with a hard output-size cap (Node's native
|
|
274
|
+
// ERR_BUFFER_TOO_LARGE on overflow — no custom streaming counter needed).
|
|
275
|
+
// Returns the input unchanged when it isn't gzip-magic (plain tar).
|
|
276
|
+
function decompressIfGzip(buf, limits = DEFAULT_LIMITS) {
|
|
277
|
+
if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {
|
|
278
|
+
try {
|
|
279
|
+
return node_zlib__WEBPACK_IMPORTED_MODULE_0__.gunzipSync(buf, { maxOutputLength: limits.maxGunzipOutput });
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (err && err.code === 'ERR_BUFFER_TOO_LARGE') {
|
|
282
|
+
throw new TarSafetyError('decompressed layer exceeds the decompression-bomb guard', 'gunzip_too_large');
|
|
283
|
+
}
|
|
284
|
+
throw new TarSafetyError(`gzip stream is malformed: ${err.message}`, 'gunzip_malformed');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return buf;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Reads a decompressed tar buffer and yields one record per entry:
|
|
292
|
+
* { name, size, typeflag, linkname, data, unsafe }
|
|
293
|
+
* `data` is a Buffer view into `buf` (never copied) for safe entries, `null`
|
|
294
|
+
* for unsafe ones. Throws TarSafetyError (never returns a partial result) the
|
|
295
|
+
* moment any configured limit is exceeded.
|
|
296
|
+
*/
|
|
297
|
+
function* readTarEntries(buf, limits = DEFAULT_LIMITS) {
|
|
298
|
+
if (!Buffer.isBuffer(buf)) throw new TarSafetyError('tar input must be a Buffer', 'bad_input');
|
|
299
|
+
let offset = 0;
|
|
300
|
+
let count = 0;
|
|
301
|
+
let totalSize = 0;
|
|
302
|
+
let pendingLongName = null;
|
|
303
|
+
|
|
304
|
+
while (offset + BLOCK <= buf.length) {
|
|
305
|
+
const header = buf.subarray(offset, offset + BLOCK);
|
|
306
|
+
if (header.every((b) => b === 0)) {
|
|
307
|
+
offset += BLOCK;
|
|
308
|
+
continue; // end-of-archive zero-fill padding
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
count += 1;
|
|
312
|
+
if (count > limits.maxEntries) {
|
|
313
|
+
throw new TarSafetyError(`tar archive has more than ${limits.maxEntries} entries`, 'too_many_entries');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let name = cstr(header.subarray(0, 100));
|
|
317
|
+
const typeflag = String.fromCharCode(header[156] || 0);
|
|
318
|
+
const linkname = cstr(header.subarray(157, 257));
|
|
319
|
+
const magic = header.subarray(257, 263).toString('ascii');
|
|
320
|
+
const prefix = magic.startsWith('ustar') ? cstr(header.subarray(345, 500)) : '';
|
|
321
|
+
if (prefix) name = `${prefix}/${name}`;
|
|
322
|
+
const size = parseSizeField(header.subarray(124, 136));
|
|
323
|
+
|
|
324
|
+
offset += BLOCK;
|
|
325
|
+
|
|
326
|
+
if (size < 0 || !Number.isFinite(size)) {
|
|
327
|
+
throw new TarSafetyError(`tar entry "${name}" has an invalid size field`, 'bad_size');
|
|
328
|
+
}
|
|
329
|
+
if (size > limits.maxEntrySize) {
|
|
330
|
+
throw new TarSafetyError(`tar entry "${name}" (${size} bytes) exceeds the per-entry size limit`, 'entry_too_large');
|
|
331
|
+
}
|
|
332
|
+
totalSize += size;
|
|
333
|
+
if (totalSize > limits.maxTotalSize) {
|
|
334
|
+
throw new TarSafetyError('tar archive exceeds the total-size limit', 'archive_too_large');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const dataBlocks = Math.ceil(size / BLOCK);
|
|
338
|
+
if (offset + size > buf.length) {
|
|
339
|
+
throw new TarSafetyError(`tar entry "${name}" claims more data than the archive contains`, 'truncated');
|
|
340
|
+
}
|
|
341
|
+
const data = size > 0 ? buf.subarray(offset, offset + size) : Buffer.alloc(0);
|
|
342
|
+
offset += dataBlocks * BLOCK;
|
|
343
|
+
|
|
344
|
+
const resolvedName = pendingLongName ? pendingLongName : name;
|
|
345
|
+
pendingLongName = null;
|
|
346
|
+
|
|
347
|
+
if (typeflag === 'L') {
|
|
348
|
+
// GNU longname: this entry's data IS the next entry's real name.
|
|
349
|
+
pendingLongName = cstr(data);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (typeflag === 'K' || typeflag === 'x' || typeflag === 'g') {
|
|
353
|
+
// GNU longlink / PAX extended header — structurally skipped (see header comment).
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!isSafeEntryName(resolvedName)) {
|
|
358
|
+
yield { name: resolvedName, size, typeflag, linkname, data: null, unsafe: true };
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
yield { name: resolvedName.replace(/\\/g, '/'), size, typeflag, linkname, data, unsafe: false };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
/***/ })
|
|
368
|
+
|
|
369
|
+
};
|
package/dist/9382.index.js
CHANGED
|
@@ -421,7 +421,8 @@ async function adjudicate(candidates, opts = {}) {
|
|
|
421
421
|
origin: 'context-intelligence', questionPack: c.questionPack, witnesses: c.witnesses, witnessCount: c.witnesses.length,
|
|
422
422
|
sourceSpanEvidence: c.sourceSpanEvidence, scopeIntegrity: c.scopeIntegrity, reviewStatus: c.reviewStatus,
|
|
423
423
|
uncertainties: c.uncertainties, recommendedValidation: c.recommendedValidation,
|
|
424
|
-
|
|
424
|
+
...(c.deterministic ? { deterministic: c.deterministic } : {}),
|
|
425
|
+
note: c.deterministic ? 'Derived from the code analysis and a deterministic counterexample attempt; no model produced this. Severity comes from the confirmation tier only.' : 'A model hypothesis checked for citations, not a proof. Severity comes from the confirmation tier only.',
|
|
425
426
|
},
|
|
426
427
|
};
|
|
427
428
|
};
|
|
@@ -774,7 +775,8 @@ const BUILD = [
|
|
|
774
775
|
{ language: 'javascript/typescript', kind: 'http', frameworks: 'Express-style call registration (get/post/put/patch/delete/all, router.use, app.use)', status: 'verified', evidence: [T('build-js.test.js')], limits: 'Router mounts in another file and middleware factories are not followed (reported as router-mount-not-resolved); destructured and named imports are followed, re-exports and tsconfig path aliases are not.' },
|
|
775
776
|
{ language: 'python', kind: 'http', frameworks: 'decorator registration (Flask/FastAPI-style @app.route / @router.get), FastAPI Depends()/Security() guards', status: 'experimental', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Decorator guards are read from the decorator lines and Depends() guards from the signature (only dependencies named like an auth or user provider bind the principal); framework-level before_request hooks and Django URL conf are not modelled.' },
|
|
776
777
|
{ language: 'haskell', kind: 'http', frameworks: 'Scotty, WAI, Servant, Yesod (through the existing web model)', status: 'verified', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Static source analysis; the existing web model decides what a route is. Dynamic routes are reported unresolved.' },
|
|
777
|
-
{ language: '
|
|
778
|
+
{ language: 'php', kind: 'http', frameworks: 'Laravel Route:: registration (the entry point only)', status: 'experimental', evidence: ['test/engineering-excellence/e4-01-dci-audit.test.js'], limits: 'Measured by the E4-01 audit on one fixture: the routes resolve, but no control or sensitive operation was extracted from the handlers.' },
|
|
779
|
+
{ language: 'java/kotlin/c#/go/ruby/rust', kind: 'http', frameworks: 'whatever the engine route scan recognises', status: 'unsupported', evidence: [], limits: 'Measured by the E4-01 audit (docs/engineering-excellence/DCI_MATURITY.md) on one fixture per language (Spring, net/http, ASP.NET, Rails): no entry point was found, so no bundle is built.' },
|
|
778
780
|
{ language: 'any with a recognised surface', kind: 'webhook/queue/cron/upload/cli', frameworks: 'existing entry-point inventory', status: 'experimental', evidence: [T('build-js.test.js')], limits: 'Handler is the enclosing declaration of the surface; no framework wiring beyond that.' },
|
|
779
781
|
{ language: 'haskell', kind: 'compiler metadata import', frameworks: 'hie-export/1 JSON export of GHC declarations and references', status: 'experimental', evidence: [T('hie-adapter.test.js')], limits: 'Reads an export; never runs GHC or hiedb. The export is validated against the source snapshot. No export produced by a real GHC run is checked in.' },
|
|
780
782
|
{ language: 'any', kind: 'compiler metadata build', frameworks: 'automatic metadata generation', status: 'blocked', evidence: [], limits: 'Running a compiler on untrusted source needs a sandbox proof this build does not have (Template Haskell, custom Setup and plugins run code).' },
|
|
@@ -1146,6 +1148,8 @@ async function cmdReview(root, args, format) {
|
|
|
1146
1148
|
maxCalls: num(flag(args, 'max-calls')) || config.review.maxCalls,
|
|
1147
1149
|
maxWallSeconds: num(flag(args, 'max-wall-seconds')) || config.review.maxWallSeconds,
|
|
1148
1150
|
maxTokensPerJob: config.review.maxTokensPerJob,
|
|
1151
|
+
maxTokensPerRepo: config.review.maxTokensPerRepo,
|
|
1152
|
+
anonymizePrompts: config.review.anonymizePrompts,
|
|
1149
1153
|
maxRetries: config.review.maxRetries,
|
|
1150
1154
|
jobTimeoutMs: (num(flag(args, 'job-timeout-seconds')) || 120) * 1000,
|
|
1151
1155
|
maxCostUsd: num(flag(args, 'max-cost-usd')) ?? config.review.maxCostUsd,
|
|
@@ -1258,7 +1262,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
1258
1262
|
maxFiles: 3000,
|
|
1259
1263
|
semanticMetadata: { mode: 'off', haskellHieDir: null },
|
|
1260
1264
|
context: { profile: 'standard', maxTokens: null, pruneLowValue: true, maxSymbols: 60, maxDepth: 12 },
|
|
1261
|
-
review: { enabled: false, allowRemote: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3 },
|
|
1265
|
+
review: { enabled: false, allowRemote: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3, maxTokensPerRepo: null, anonymizePrompts: false },
|
|
1262
1266
|
replay: { contentAddressed: true, retention: 'inherit' },
|
|
1263
1267
|
});
|
|
1264
1268
|
|
|
@@ -1307,6 +1311,8 @@ function normalizeConfig(raw) {
|
|
|
1307
1311
|
maxCostUsd: Number.isFinite(rv.maxCostUsd) && rv.maxCostUsd >= 0 ? rv.maxCostUsd : null,
|
|
1308
1312
|
maxTokensPerJob: clamp(rv.maxTokensPerJob, 1000, 400000, d.review.maxTokensPerJob),
|
|
1309
1313
|
maxRetries: clamp(rv.maxRetries, 0, 3, d.review.maxRetries),
|
|
1314
|
+
maxTokensPerRepo: Number.isFinite(rv.maxTokensPerRepo) && rv.maxTokensPerRepo >= 1000 ? Math.trunc(rv.maxTokensPerRepo) : null,
|
|
1315
|
+
anonymizePrompts: rv.anonymizePrompts === true,
|
|
1310
1316
|
},
|
|
1311
1317
|
replay: { contentAddressed: rp.contentAddressed !== false, retention: typeof rp.retention === 'string' ? rp.retention : 'inherit' },
|
|
1312
1318
|
};
|
|
@@ -1333,6 +1339,7 @@ function loadConfig(root) {
|
|
|
1333
1339
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
1334
1340
|
|
|
1335
1341
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
1342
|
+
/* harmony export */ $E: () => (/* binding */ blankComments),
|
|
1336
1343
|
/* harmony export */ TM: () => (/* binding */ looksLikeTelemetry),
|
|
1337
1344
|
/* harmony export */ gs: () => (/* binding */ signatureText),
|
|
1338
1345
|
/* harmony export */ kl: () => (/* binding */ analyzeSymbolText),
|
|
@@ -1480,6 +1487,51 @@ function clip(s) { return s.trim().slice(0, 160); }
|
|
|
1480
1487
|
* lines the FILE's lines (0-based array)
|
|
1481
1488
|
* Returns [{ symbolId, line, category:'control'|'sensitive', kind, subkind, evidence, basis, rejectionSeen, text }]
|
|
1482
1489
|
*/
|
|
1490
|
+
/**
|
|
1491
|
+
* Blank the comments (and Python docstrings) of a source file, keeping every character position and newline, so line numbers do not move.
|
|
1492
|
+
* The analysis looks for calls and comparisons in code; a comment that SAYS `ownerId: req.user.id` or `requireUser(req)` is prose, and prose is the
|
|
1493
|
+
* easiest thing for a hostile or merely careless author to write. Strings are left alone (the principal and SQL rules already read around them).
|
|
1494
|
+
*/
|
|
1495
|
+
function blankComments(text, file = '') {
|
|
1496
|
+
const src = String(text || '');
|
|
1497
|
+
const hash = /\.(?:py|rb|sh|php|pl|r)$/i.test(file);
|
|
1498
|
+
const dashes = /\.l?hs$/i.test(file);
|
|
1499
|
+
const cLike = !dashes && !/\.(?:py|rb|sh|pl|r)$/i.test(file);
|
|
1500
|
+
const out = src.split('');
|
|
1501
|
+
const n = src.length;
|
|
1502
|
+
let i = 0;
|
|
1503
|
+
const blank = (a, b) => { for (let k = a; k < b && k < n; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
1504
|
+
while (i < n) {
|
|
1505
|
+
const c = src[i]; const d = src[i + 1];
|
|
1506
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
1507
|
+
if (/\.py$/i.test(file) && src.startsWith(c.repeat(3), i)) {
|
|
1508
|
+
const end = src.indexOf(c.repeat(3), i + 3); const stop = end < 0 ? n : end + 3;
|
|
1509
|
+
blank(i, stop); i = stop; continue; // a docstring or triple-quoted block is prose
|
|
1510
|
+
}
|
|
1511
|
+
let j = i + 1;
|
|
1512
|
+
while (j < n && src[j] !== c && !(src[j] === '\n' && c !== '`')) { if (src[j] === '\\') j++; j++; }
|
|
1513
|
+
i = j + 1; continue;
|
|
1514
|
+
}
|
|
1515
|
+
if (cLike && c === '/' && d === '*') { const end = src.indexOf('*/', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
|
|
1516
|
+
if (cLike && c === '/' && d === '/' && (i === 0 || /[\s;,)}{\]]/.test(src[i - 1]))) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
|
|
1517
|
+
if (hash && c === '#' && !(file.endsWith('.php') && d === '[')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
|
|
1518
|
+
if (dashes && c === '-' && d === '-' && !/[!#$%&*+./<=>?@\\^|~:]/.test(src[i + 2] || ' ')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
|
|
1519
|
+
if (dashes && c === '{' && d === '-') { const end = src.indexOf('-}', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
|
|
1520
|
+
i++;
|
|
1521
|
+
}
|
|
1522
|
+
return out.join('');
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function bodyAfterSignature(raw) {
|
|
1526
|
+
const arrow = raw.indexOf('=>');
|
|
1527
|
+
let rest = null;
|
|
1528
|
+
if (arrow >= 0) rest = raw.slice(arrow + 2);
|
|
1529
|
+
else { const m = /\)\s*\{(.*)$/.exec(raw); if (m) rest = m[1]; }
|
|
1530
|
+
if (rest === null) return '';
|
|
1531
|
+
const brace = /^\s*\{(.*)$/.exec(rest);
|
|
1532
|
+
return brace ? brace[1] : rest;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1483
1535
|
function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
|
|
1484
1536
|
const out = [];
|
|
1485
1537
|
const seen = new Set();
|
|
@@ -1512,7 +1564,10 @@ function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
|
|
|
1512
1564
|
const calls = [];
|
|
1513
1565
|
const re = /([A-Za-z_$][\w$]*(?:(?:\.|::)[A-Za-z_$][\w$]*)*)\s*\(/g;
|
|
1514
1566
|
let m;
|
|
1515
|
-
|
|
1567
|
+
// A one-line handler (`app.get('/x', guard, async (req, res) => { res.json(await db.docs.findOne({ id })); })`) is its own declaration
|
|
1568
|
+
// line, so skipping the whole line also skipped its body. Only the SIGNATURE is skipped: the calls after the arrow or the opening brace count.
|
|
1569
|
+
const scanText = isDeclLine ? bodyAfterSignature(raw) : raw;
|
|
1570
|
+
while ((m = re.exec(scanText))) calls.push(m[1]);
|
|
1516
1571
|
if (haskell && !isDeclLine) {
|
|
1517
1572
|
const hre = /\b([A-Z][\w.]*\.[a-z][\w']*|[a-z][\w']*)\b/g;
|
|
1518
1573
|
while ((m = hre.exec(raw))) if (AUTH_NAME.test(m[1]) || HS_SENSITIVE.test(m[1])) calls.push(m[1]);
|
|
@@ -3021,6 +3076,8 @@ async function runQueue(jobs, ctx) {
|
|
|
3021
3076
|
maxCalls: Math.min(HARD_LIMITS.maxCalls, Math.max(1, ctx.limits.maxCalls || 100)),
|
|
3022
3077
|
maxWallMs: Math.min(HARD_LIMITS.maxWallSeconds, Math.max(1, ctx.limits.maxWallSeconds || 900)) * 1000,
|
|
3023
3078
|
maxTokensPerJob: ctx.limits.maxTokensPerJob || 40000,
|
|
3079
|
+
maxTokensPerRepo: Number.isFinite(ctx.limits.maxTokensPerRepo) ? ctx.limits.maxTokensPerRepo : null,
|
|
3080
|
+
anonymizePrompts: ctx.limits.anonymizePrompts === true,
|
|
3024
3081
|
maxRetries: Math.min(HARD_LIMITS.maxRetries, Math.max(0, ctx.limits.maxRetries ?? 3)),
|
|
3025
3082
|
jobTimeoutMs: ctx.limits.jobTimeoutMs || 120000,
|
|
3026
3083
|
maxCostUsd: Number.isFinite(ctx.limits.maxCostUsd) ? ctx.limits.maxCostUsd : null,
|
|
@@ -3034,6 +3091,7 @@ async function runQueue(jobs, ctx) {
|
|
|
3034
3091
|
const results = new Map();
|
|
3035
3092
|
let calls = 0;
|
|
3036
3093
|
let estCost = 0;
|
|
3094
|
+
let tokensUsed = 0;
|
|
3037
3095
|
let cooldownUntil = 0;
|
|
3038
3096
|
let budgetStop = null;
|
|
3039
3097
|
let persistChain = Promise.resolve();
|
|
@@ -3083,6 +3141,7 @@ async function runQueue(jobs, ctx) {
|
|
|
3083
3141
|
if (ctx.signal && ctx.signal.aborted) return 'cancelled';
|
|
3084
3142
|
if (now() - started >= L.maxWallMs) return 'budget-exhausted:wall-time';
|
|
3085
3143
|
if (calls >= L.maxCalls) return 'budget-exhausted:max-calls';
|
|
3144
|
+
if (L.maxTokensPerRepo !== null && tokensUsed >= L.maxTokensPerRepo) return 'budget-exhausted:max-tokens-per-repo';
|
|
3086
3145
|
if (L.maxCostUsd !== null && L.costPerCallUsd !== null && estCost + L.costPerCallUsd > L.maxCostUsd) return 'budget-exhausted:max-cost';
|
|
3087
3146
|
return null;
|
|
3088
3147
|
};
|
|
@@ -3106,8 +3165,9 @@ async function runQueue(jobs, ctx) {
|
|
|
3106
3165
|
state.jobs[job.key] = { state: 'running', reason: null, attempts, label: job.label };
|
|
3107
3166
|
const out = await (0,_review_js__WEBPACK_IMPORTED_MODULE_2__/* .runReviewOnce */ .z6)({
|
|
3108
3167
|
pack: loadedJob.pack, entrypoints: loadedJob.entrypoints, manifests: loadedJob.manifests, packageIndexes: job.packageIndexes, packageTexts: loadedJob.packageTexts,
|
|
3109
|
-
fileContents: ctx.fileContents, scanRoot: ctx.root, invoker: ctx.invoker, model: ctx.model, limits: { maxTokensPerJob: L.maxTokensPerJob, jobTimeoutMs: L.jobTimeoutMs },
|
|
3168
|
+
fileContents: ctx.fileContents, scanRoot: ctx.root, invoker: ctx.invoker, model: ctx.model, limits: { maxTokensPerJob: L.maxTokensPerJob, jobTimeoutMs: L.jobTimeoutMs, anonymizePrompts: L.anonymizePrompts },
|
|
3110
3169
|
});
|
|
3170
|
+
if (out.usage) tokensUsed += (out.usage.inputTokensEstimated || 0) + (out.usage.outputTokensEstimated || 0);
|
|
3111
3171
|
if (out.state === 'skipped_by_policy') calls--; // nothing was sent
|
|
3112
3172
|
if (out.state === 'failed' && out.retryable && attempts <= L.maxRetries) {
|
|
3113
3173
|
// exponential backoff with jitter; the shared cooldown slows every worker, which is what a 429 storm needs
|
|
@@ -3284,7 +3344,7 @@ function reachFrom({ graph, fileContents, entrypoint, limits = {} }) {
|
|
|
3284
3344
|
const lim = { ...DEFAULT_LIMITS, ...limits };
|
|
3285
3345
|
const { byId, edgesFrom } = indexGraph(graph);
|
|
3286
3346
|
const linesCache = new Map();
|
|
3287
|
-
const linesOf = (f) => { if (!linesCache.has(f)) linesCache.set(f, (fileContents[f] || '').split('\n')); return linesCache.get(f); };
|
|
3347
|
+
const linesOf = (f) => { if (!linesCache.has(f)) linesCache.set(f, (0,_controls_js__WEBPACK_IMPORTED_MODULE_0__/* .blankComments */ .$E)(fileContents[f] || '', f).split('\n')); return linesCache.get(f); };
|
|
3288
3348
|
|
|
3289
3349
|
const visited = [];
|
|
3290
3350
|
const seen = new Set();
|
|
@@ -3450,7 +3510,7 @@ function evaluateProtection({ facts, unknowns, omittedWithControls = false, capp
|
|
|
3450
3510
|
/* harmony export */ sj: () => (/* binding */ verifyReview),
|
|
3451
3511
|
/* harmony export */ z6: () => (/* binding */ runReviewOnce)
|
|
3452
3512
|
/* harmony export */ });
|
|
3453
|
-
/* unused harmony exports CONCLUSIONS, LIKELIHOODS, isLoopbackEndpoint, claimGrounded, resolveCitation, isRetryableError */
|
|
3513
|
+
/* unused harmony exports CONCLUSIONS, LIKELIHOODS, isLoopbackEndpoint, stripUntrustedCommands, pseudonymizer, claimGrounded, resolveCitation, isRetryableError */
|
|
3454
3514
|
/* harmony import */ var _util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(17097);
|
|
3455
3515
|
/* harmony import */ var _egress_redact_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11723);
|
|
3456
3516
|
/* harmony import */ var _egress_policy_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(45712);
|
|
@@ -3599,12 +3659,69 @@ function checkEgressForFiles(files, { scanRoot, model }) {
|
|
|
3599
3659
|
// other endpoint needs --allow-remote (or review.allowRemote), on top of the egress policy below, never instead of it.
|
|
3600
3660
|
if (model.remote && !model.remoteApproved) return { allowed: false, reason: 'remote-endpoint-not-approved (pass --allow-remote or set review.allowRemote after reviewing the egress policy)', file: files[0] || null, evaluated: false };
|
|
3601
3661
|
for (const file of files) {
|
|
3602
|
-
|
|
3603
|
-
|
|
3662
|
+
// Fail closed. A policy check that throws (a corrupt policy file, an unreadable path, a bug) is not an allow: the answer is "deny", with the
|
|
3663
|
+
// reason, and nothing is sent. This is the only place that decides whether a prompt may leave the machine.
|
|
3664
|
+
let d;
|
|
3665
|
+
try { d = (0,_egress_policy_js__WEBPACK_IMPORTED_MODULE_2__/* .evaluateEgress */ .nn)({ scanRoot, purpose: 'context-review', endpoint: model.endpoint, provider: model.provider, model: model.model, path: file }); }
|
|
3666
|
+
catch (err) { return { allowed: false, reason: `egress-policy-error: ${String((err && err.message) || err).slice(0, 120)}`, file, evaluated: true, failedClosed: true }; }
|
|
3667
|
+
if (!d || d.allowed !== true) return { allowed: false, reason: (d && d.reason) || 'egress-policy-returned-no-decision', file, evaluated: true, ...(d ? {} : { failedClosed: true }) };
|
|
3604
3668
|
}
|
|
3605
3669
|
return { allowed: true, evaluated: true };
|
|
3606
3670
|
}
|
|
3607
3671
|
|
|
3672
|
+
// ---- model output hygiene -------------------------------------------------------------------------------------
|
|
3673
|
+
|
|
3674
|
+
/**
|
|
3675
|
+
* A model's text is untrusted. It can carry a command for a human or a script to run ("run curl ... | sh to confirm"). Nothing here executes model text, but
|
|
3676
|
+
* these strings reach reports, tickets and fix suggestions that people copy from, so shell-looking content is removed and counted, never passed on.
|
|
3677
|
+
*/
|
|
3678
|
+
const FENCE = /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
|
|
3679
|
+
const CMD_WORD = '(?:curl|wget|bash|sh|zsh|sudo|su|rm|chmod|chown|nc|ncat|netcat|python3?|node|perl|ruby|php|powershell|pwsh|cmd(?:\\.exe)?|eval|exec|scp|ssh|dd|mkfs|kill|pkill|git\\s+push|npm\\s+(?:i|install|publish)|pip3?\\s+install|docker\\s+run)';
|
|
3680
|
+
const INLINE_CMD = new RegExp('`[^`\\n]*\\b' + CMD_WORD + '\\b[^`\\n]*`|\\$\\([^)]*\\)|\\b(?:curl|wget)\\s+[^\\s]+[^\\n]*\\|\\s*(?:sh|bash)\\b|(?:^|\\n)\\s*(?:\\$|#)\\s+' + CMD_WORD + '\\b[^\\n]*', 'gi');
|
|
3681
|
+
function stripUntrustedCommands(text) {
|
|
3682
|
+
if (typeof text !== 'string' || !text) return { text: text ?? '', stripped: 0 };
|
|
3683
|
+
let n = 0;
|
|
3684
|
+
let out = text.replace(FENCE, () => { n++; return '[command block removed]'; });
|
|
3685
|
+
out = out.replace(INLINE_CMD, () => { n++; return '[command removed]'; });
|
|
3686
|
+
return { text: out, stripped: n };
|
|
3687
|
+
}
|
|
3688
|
+
function sanitizeReviewStrings(review) {
|
|
3689
|
+
let total = 0;
|
|
3690
|
+
const walk = (v, key) => {
|
|
3691
|
+
if (typeof v === 'string') {
|
|
3692
|
+
if (['promptHash', 'responseHash', 'contextHash', 'entrypointId', 'file', 'role', 'kind', 'conclusion', 'likelihood', 'questionPack', 'status'].includes(key)) return v;
|
|
3693
|
+
const r = stripUntrustedCommands(v); total += r.stripped; return r.text;
|
|
3694
|
+
}
|
|
3695
|
+
if (Array.isArray(v)) return v.map((x) => walk(x, key));
|
|
3696
|
+
if (v && typeof v === 'object') { const o = {}; for (const [k, x] of Object.entries(v)) o[k] = walk(x, k); return o; }
|
|
3697
|
+
return v;
|
|
3698
|
+
};
|
|
3699
|
+
const out = walk(review, '');
|
|
3700
|
+
if (total) out.untrustedCommandsStripped = total;
|
|
3701
|
+
return out;
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
/** Where a suggestion came from and what checked it. Attached to every review so a reader never mistakes a model's words for a finding. */
|
|
3705
|
+
function provenanceOf(model, promptHash) {
|
|
3706
|
+
let host = null; try { host = model.endpoint ? new URL(model.endpoint).hostname : null; } catch { host = null; }
|
|
3707
|
+
const loopback = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
3708
|
+
return { producer: 'model', provider: model.provider ?? null, model: model.model ?? null, endpointClass: !model.endpoint ? 'none' : loopback ? 'loopback' : 'remote', promptHash, checkedBy: 'deterministic-citation-verifier', promotedToFinding: false };
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// ---- prompt anonymization ---------------------------------------------------------------------------------------
|
|
3712
|
+
|
|
3713
|
+
/** Replace each real file path with a stable pseudonym (`f_<hash>.<ext>`). The mapping stays in memory; only the pseudonyms leave. */
|
|
3714
|
+
function pseudonymizer(files) {
|
|
3715
|
+
const toPseudo = new Map(); const toReal = new Map();
|
|
3716
|
+
for (const f of [...new Set(files)].sort()) {
|
|
3717
|
+
const ext = (/\.[A-Za-z0-9]{1,6}$/.exec(f) || [''])[0];
|
|
3718
|
+
const p = `f_${(0,_canonical_js__WEBPACK_IMPORTED_MODULE_5__/* .sha256 */ .sc)(f).slice(0, 10)}${ext}`;
|
|
3719
|
+
toPseudo.set(f, p); toReal.set(p, f);
|
|
3720
|
+
}
|
|
3721
|
+
const swap = (text, map) => { let t = text; for (const [a, b] of [...map].sort((x, y) => y[0].length - x[0].length)) t = t.split(a).join(b); return t; };
|
|
3722
|
+
return { toPseudo: (t) => swap(t, toPseudo), toReal: (t) => swap(t, toReal), size: toPseudo.size };
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3608
3725
|
// ---- response parsing and verification ------------------------------------------------------------------------
|
|
3609
3726
|
|
|
3610
3727
|
function extractJson(raw) {
|
|
@@ -3813,7 +3930,12 @@ async function runReviewOnce({ pack, entrypoints, manifests, packageIndexes, pac
|
|
|
3813
3930
|
const badRedact = redacted.find(r => !r.ok);
|
|
3814
3931
|
if (badRedact) return { state: 'skipped_by_policy', reason: `redaction refused: ${badRedact.reason}` };
|
|
3815
3932
|
|
|
3816
|
-
|
|
3933
|
+
// Policy-driven anonymization: file paths are replaced by pseudonyms before the prompt is built, and mapped back in the answer before its citations
|
|
3934
|
+
// are checked against the real snapshot. The model never sees a real path, and what is hashed is what was sent.
|
|
3935
|
+
const anon = limits.anonymizePrompts ? pseudonymizer(files) : null;
|
|
3936
|
+
const sendTexts = anon ? redacted.map(r => anon.toPseudo(r.text)) : redacted.map(r => r.text);
|
|
3937
|
+
const sendEntrypoints = anon ? entrypoints.map(e => JSON.parse(anon.toPseudo(JSON.stringify(e)))) : entrypoints;
|
|
3938
|
+
const { prompt, promptHash } = buildReviewPrompt({ pack, entrypoints: sendEntrypoints, packageTexts: sendTexts, contextHash: manifests[0].contextHash });
|
|
3817
3939
|
const est = (0,_packer_js__WEBPACK_IMPORTED_MODULE_6__/* .estimateTokens */ .bP)(prompt);
|
|
3818
3940
|
if (est.tokens > limits.maxTokensPerJob) return { state: 'skipped_by_policy', reason: `prompt is about ${est.tokens} tokens, over the ${limits.maxTokensPerJob} per-job limit` };
|
|
3819
3941
|
|
|
@@ -3825,7 +3947,9 @@ async function runReviewOnce({ pack, entrypoints, manifests, packageIndexes, pac
|
|
|
3825
3947
|
return { state: 'failed', reason: `model call failed: ${String((err && err.message) || err).slice(0, 160)}`, retryable: isRetryableError(err) };
|
|
3826
3948
|
}
|
|
3827
3949
|
const usage = { inputTokensEstimated: est.tokens, outputTokensEstimated: Math.ceil(String(raw || '').length / 4), source: 'estimate', redactions: redacted.reduce((n, r) => n + r.redactions, 0) };
|
|
3828
|
-
const
|
|
3950
|
+
const rawReal = anon && typeof raw === 'string' ? anon.toReal(raw) : raw;
|
|
3951
|
+
const review0 = verifyReview({ raw: rawReal, pack, entrypoints, manifests, packageIndexes, fileContents, promptHash });
|
|
3952
|
+
const review = { ...sanitizeReviewStrings(review0), modelProvenance: provenanceOf(model, promptHash), ...(anon ? { promptAnonymized: true } : {}) };
|
|
3829
3953
|
if (review.status === 'declined') return { state: 'partial', reason: `answer declined: ${review.declineReasons.join(',')}`, review, usage };
|
|
3830
3954
|
return { state: 'succeeded', reason: null, review, usage };
|
|
3831
3955
|
}
|