@bongos/core 1.19.200 → 1.19.202
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/.bongos-core.json +11 -11
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +15 -1
- package/modules/security/artifact-scan/floor.js +57 -3
- package/modules/security/tests/floor.mjs +50 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.202",
|
|
6
|
+
"core_contract": "1.19.202",
|
|
7
|
+
"source_commit": "241b029c800ce3bdb38f7d581e44d95d27656007",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-17T19:
|
|
9
|
+
"built_at": "2026-08-17T19:15:32.076Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 310,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1625,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "d0af1afec31904ff2d8c0ba6a68c5960c6b333ef65c66938843f2a7bd681f4f0",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4047,7 +4047,7 @@
|
|
|
4047
4047
|
{
|
|
4048
4048
|
"path": "modules/security/artifact-scan/detectors/npm-lifecycle.js",
|
|
4049
4049
|
"mode": "0000644",
|
|
4050
|
-
"sha256": "
|
|
4050
|
+
"sha256": "97977c34dd71368a980e8eea568ad63c79aeb00a9e33eb93bf85075367f4b3c8"
|
|
4051
4051
|
},
|
|
4052
4052
|
{
|
|
4053
4053
|
"path": "modules/security/artifact-scan/detectors/permissions.js",
|
|
@@ -4072,7 +4072,7 @@
|
|
|
4072
4072
|
{
|
|
4073
4073
|
"path": "modules/security/artifact-scan/floor.js",
|
|
4074
4074
|
"mode": "0000644",
|
|
4075
|
-
"sha256": "
|
|
4075
|
+
"sha256": "27421162e08ed14ba9e26be02e11c936c566ad113b76a90d452474d7e867093b"
|
|
4076
4076
|
},
|
|
4077
4077
|
{
|
|
4078
4078
|
"path": "modules/security/artifact-scan/limits.js",
|
|
@@ -4157,7 +4157,7 @@
|
|
|
4157
4157
|
{
|
|
4158
4158
|
"path": "modules/security/tests/floor.mjs",
|
|
4159
4159
|
"mode": "0000644",
|
|
4160
|
-
"sha256": "
|
|
4160
|
+
"sha256": "7eaed4b59cce956509ca7ebcb92207d362f191ec9b54be90d3c47d069b8149e7"
|
|
4161
4161
|
},
|
|
4162
4162
|
{
|
|
4163
4163
|
"path": "modules/security/tests/mirror.mjs",
|
|
@@ -4272,12 +4272,12 @@
|
|
|
4272
4272
|
{
|
|
4273
4273
|
"path": "package-lock.json",
|
|
4274
4274
|
"mode": "0000644",
|
|
4275
|
-
"sha256": "
|
|
4275
|
+
"sha256": "28f5946694aefef1201397cdb8962d38b2b1d5d0904ef6f76b5b1591f0f6a2e8"
|
|
4276
4276
|
},
|
|
4277
4277
|
{
|
|
4278
4278
|
"path": "package.json",
|
|
4279
4279
|
"mode": "0000644",
|
|
4280
|
-
"sha256": "
|
|
4280
|
+
"sha256": "cbf3b235c2ed444a8bf84a0719df6fbe81f8ad98dcbbf5eaf3740338af1e53a3"
|
|
4281
4281
|
},
|
|
4282
4282
|
{
|
|
4283
4283
|
"path": "public-docs/index.html",
|
|
@@ -5887,7 +5887,7 @@
|
|
|
5887
5887
|
{
|
|
5888
5888
|
"path": "src/module-api.js",
|
|
5889
5889
|
"mode": "0000644",
|
|
5890
|
-
"sha256": "
|
|
5890
|
+
"sha256": "2cd85c33fc8008eec96025eeaaa528590157902642653819bf20b580e1d031a5"
|
|
5891
5891
|
},
|
|
5892
5892
|
{
|
|
5893
5893
|
"path": "src/module-loader/catalog.js",
|
|
@@ -26,6 +26,20 @@ const AUTO_RUN_SCRIPTS = Object.freeze([
|
|
|
26
26
|
'dependencies',
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
+
// When each hook actually fires. All are auto-run (same severity), but calling a
|
|
30
|
+
// publish-time hook "install time" overstates the risk to the INSTALLING user —
|
|
31
|
+
// the message must name the real trigger. `prepare` fires on both (install from
|
|
32
|
+
// git, and publish); the deprecated `prepublish` no longer runs on install in
|
|
33
|
+
// modern npm.
|
|
34
|
+
const SCRIPT_TIMING = Object.freeze({
|
|
35
|
+
prepublish: 'publish/pack time',
|
|
36
|
+
prepublishOnly: 'publish/pack time',
|
|
37
|
+
prepack: 'publish/pack time',
|
|
38
|
+
postpack: 'publish/pack time',
|
|
39
|
+
prepare: 'install (git deps) and publish time',
|
|
40
|
+
});
|
|
41
|
+
const timingOf = (name) => SCRIPT_TIMING[name] || 'install time';
|
|
42
|
+
|
|
29
43
|
function detect(corpus) {
|
|
30
44
|
const findings = [];
|
|
31
45
|
for (const file of corpus.files) {
|
|
@@ -43,7 +57,7 @@ function detect(corpus) {
|
|
|
43
57
|
for (const name of AUTO_RUN_SCRIPTS) {
|
|
44
58
|
if (Object.prototype.hasOwnProperty.call(scripts, name)) {
|
|
45
59
|
findings.push(finding('npm_lifecycle_script', 'high', file.path,
|
|
46
|
-
`npm auto-runs "${name}" at
|
|
60
|
+
`npm auto-runs "${name}" at ${timingOf(name)}: ${excerpt(scripts[name])}`,
|
|
47
61
|
{ script: name, command: excerpt(scripts[name]) }));
|
|
48
62
|
}
|
|
49
63
|
const rawCount = rawCounts.get(name) || 0;
|
|
@@ -191,6 +191,54 @@ function coverageFindings(corpus) {
|
|
|
191
191
|
return findings;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// Cap the merged-path list so an artifact vendoring thousands of identical
|
|
195
|
+
// copies cannot balloon the persisted report through its own duplication.
|
|
196
|
+
const MAX_DUPLICATE_PATHS = 20;
|
|
197
|
+
|
|
198
|
+
// A repo that vendors the same tree N times produces N identical findings from
|
|
199
|
+
// one underlying file (same detector, same evidence, byte-identical content) —
|
|
200
|
+
// which triples counts and buries the real signal. Collapse findings that agree
|
|
201
|
+
// on (code, severity, message) AND whose files are byte-identical per the
|
|
202
|
+
// manifest sha256; the survivor lists the other copies. Keyed on the full
|
|
203
|
+
// message so per-path evidence (differing line numbers, labels) never merges
|
|
204
|
+
// wrongly, and findings without a manifest file entry ('(floor)', '(fetch)',
|
|
205
|
+
// directories, symlinks) pass through untouched. Verdict arithmetic is
|
|
206
|
+
// unaffected: max() over findings is idempotent under duplication.
|
|
207
|
+
function dedupeFindings(findings, entries) {
|
|
208
|
+
const shaByPath = new Map();
|
|
209
|
+
for (const e of entries || []) {
|
|
210
|
+
if (e && e.kind === 'file' && e.sha256) shaByPath.set(scrubUntrusted(String(e.path)), e.sha256);
|
|
211
|
+
}
|
|
212
|
+
const groups = new Map();
|
|
213
|
+
const slots = [];
|
|
214
|
+
for (const f of findings) {
|
|
215
|
+
const sha = f && typeof f.file === 'string' ? shaByPath.get(f.file) : null;
|
|
216
|
+
if (!sha) { slots.push({ finding: f }); continue; }
|
|
217
|
+
const key = JSON.stringify([f.code, f.severity, f.message, sha]);
|
|
218
|
+
const group = groups.get(key);
|
|
219
|
+
if (!group) {
|
|
220
|
+
groups.set(key, { first: f, dups: [] });
|
|
221
|
+
slots.push({ key });
|
|
222
|
+
} else {
|
|
223
|
+
group.dups.push(f.file);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return slots.map((slot) => {
|
|
227
|
+
if (slot.finding) return slot.finding;
|
|
228
|
+
const { first, dups } = groups.get(slot.key);
|
|
229
|
+
if (dups.length === 0) return first;
|
|
230
|
+
const baseDetail = first.detail && typeof first.detail === 'object' && !Array.isArray(first.detail) ? first.detail : {};
|
|
231
|
+
return {
|
|
232
|
+
...first,
|
|
233
|
+
detail: {
|
|
234
|
+
...baseDetail,
|
|
235
|
+
duplicate_files: dups.slice(0, MAX_DUPLICATE_PATHS),
|
|
236
|
+
duplicate_count: dups.length,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
194
242
|
// Run every detector crash-isolated: a detector that throws yields a finding,
|
|
195
243
|
// never a hole in the report. `detectors` is injectable so the crash-isolation
|
|
196
244
|
// contract is unit-testable with a deliberately-throwing detector.
|
|
@@ -273,15 +321,20 @@ async function runFloor({ pristineDir, quarantineRoot, entries, sha = null, cont
|
|
|
273
321
|
'The floor could not read the artifact tree at all — nothing here was reviewed.', { error: corpusError }));
|
|
274
322
|
}
|
|
275
323
|
|
|
324
|
+
// Collapse vendored-copy duplicates LAST, so every producing step above is
|
|
325
|
+
// covered; crash-isolated with the raw list as fallback (a broken dedupe must
|
|
326
|
+
// degrade to duplicate noise, never to lost findings).
|
|
327
|
+
const finalFindings = isolate('dedupe', () => dedupeFindings(findings, entries)) || findings;
|
|
328
|
+
|
|
276
329
|
const report = {
|
|
277
330
|
floor_version: FLOOR_VERSION,
|
|
278
331
|
detectors_run: DETECTORS.map(([name]) => name),
|
|
279
332
|
detector_errors: errors,
|
|
280
333
|
generated_at: new Date().toISOString(),
|
|
281
334
|
target: { sha, content_hash: contentHash },
|
|
282
|
-
findings,
|
|
283
|
-
counts: countBySeverity(
|
|
284
|
-
floor_verdict: computeFloorVerdict(
|
|
335
|
+
findings: finalFindings,
|
|
336
|
+
counts: countBySeverity(finalFindings),
|
|
337
|
+
floor_verdict: computeFloorVerdict(finalFindings),
|
|
285
338
|
// Printed, never scored — the reader's power budget for the artifact.
|
|
286
339
|
capabilities: capabilityInventory,
|
|
287
340
|
coverage: {
|
|
@@ -314,6 +367,7 @@ module.exports = {
|
|
|
314
367
|
countBySeverity,
|
|
315
368
|
coOccurrence,
|
|
316
369
|
coverageFindings,
|
|
370
|
+
dedupeFindings,
|
|
317
371
|
runDetectors,
|
|
318
372
|
runFloor,
|
|
319
373
|
};
|
|
@@ -58,6 +58,43 @@ function corpusOf(fileMap) {
|
|
|
58
58
|
const codesOf = (findings) => findings.map((f) => f.code);
|
|
59
59
|
const hasCode = (findings, code) => findings.some((f) => f.code === code);
|
|
60
60
|
|
|
61
|
+
// ---------- dedupe across byte-identical files ----------
|
|
62
|
+
|
|
63
|
+
await test('dedupe: identical findings on byte-identical files collapse to one, listing the copies', () => {
|
|
64
|
+
const entries = [
|
|
65
|
+
{ path: 'a/data.csv', kind: 'file', size: 10, sha256: 'aaa' },
|
|
66
|
+
{ path: 'b/data.csv', kind: 'file', size: 10, sha256: 'aaa' },
|
|
67
|
+
{ path: 'c/data.csv', kind: 'file', size: 10, sha256: 'aaa' },
|
|
68
|
+
{ path: 'd/other.csv', kind: 'file', size: 10, sha256: 'bbb' },
|
|
69
|
+
];
|
|
70
|
+
const mk = (file) => ({ code: 'x_hit', severity: 'critical', file, message: 'same claim', detail: { k: 1 } });
|
|
71
|
+
const input = [mk('a/data.csv'), mk('b/data.csv'), mk('c/data.csv'), mk('d/other.csv'),
|
|
72
|
+
{ code: 'detector_error', severity: 'high', file: '(floor)', message: 'crashed', detail: null }];
|
|
73
|
+
const out = floor.dedupeFindings(input, entries);
|
|
74
|
+
// vendored copies collapse; different bytes (d/) and non-file findings survive
|
|
75
|
+
assert.equal(out.length, 3);
|
|
76
|
+
const merged = out.find((f) => f.file === 'a/data.csv');
|
|
77
|
+
assert.deepEqual(merged.detail.duplicate_files, ['b/data.csv', 'c/data.csv']);
|
|
78
|
+
assert.equal(merged.detail.duplicate_count, 2);
|
|
79
|
+
assert.equal(merged.detail.k, 1, 'original detail keys survive the merge');
|
|
80
|
+
assert.ok(out.some((f) => f.file === 'd/other.csv'));
|
|
81
|
+
assert.ok(out.some((f) => f.file === '(floor)'));
|
|
82
|
+
// input findings were not mutated (the merged copy is a new object)
|
|
83
|
+
assert.equal(input[0].detail.duplicate_count, undefined);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await test('dedupe: same file bytes but different messages stay separate findings', () => {
|
|
87
|
+
const entries = [
|
|
88
|
+
{ path: 'a.js', kind: 'file', size: 5, sha256: 'ccc' },
|
|
89
|
+
{ path: 'b.js', kind: 'file', size: 5, sha256: 'ccc' },
|
|
90
|
+
];
|
|
91
|
+
const out = floor.dedupeFindings([
|
|
92
|
+
{ code: 'y', severity: 'medium', file: 'a.js', message: 'claim at line 3', detail: null },
|
|
93
|
+
{ code: 'y', severity: 'medium', file: 'b.js', message: 'claim at line 9', detail: null },
|
|
94
|
+
], entries);
|
|
95
|
+
assert.equal(out.length, 2);
|
|
96
|
+
});
|
|
97
|
+
|
|
61
98
|
// ---------- npm lifecycle ----------
|
|
62
99
|
|
|
63
100
|
await test('npm-lifecycle: quotes a postinstall verbatim as a high finding', () => {
|
|
@@ -68,6 +105,19 @@ await test('npm-lifecycle: quotes a postinstall verbatim as a high finding', ()
|
|
|
68
105
|
assert.match(hit.message, /node steal\.js/);
|
|
69
106
|
});
|
|
70
107
|
|
|
108
|
+
await test('npm-lifecycle: the message names when the hook really fires (publish/pack vs install)', () => {
|
|
109
|
+
const f = npmLifecycle.detect(corpusOf({ 'package.json': JSON.stringify({
|
|
110
|
+
scripts: { prepublishOnly: 'npm run build', postinstall: 'node setup.js' },
|
|
111
|
+
}) }));
|
|
112
|
+
const pub = f.find((x) => x.code === 'npm_lifecycle_script' && x.detail.script === 'prepublishOnly');
|
|
113
|
+
const inst = f.find((x) => x.code === 'npm_lifecycle_script' && x.detail.script === 'postinstall');
|
|
114
|
+
assert.ok(pub && inst);
|
|
115
|
+
// prepublishOnly fires at publish/pack time — calling it install-time overstates.
|
|
116
|
+
assert.match(pub.message, /publish\/pack time/);
|
|
117
|
+
assert.doesNotMatch(pub.message, /install time/);
|
|
118
|
+
assert.match(inst.message, /install time/);
|
|
119
|
+
});
|
|
120
|
+
|
|
71
121
|
await test('npm-lifecycle: a "test" script (run-by-request) is NOT flagged', () => {
|
|
72
122
|
const f = npmLifecycle.detect(corpusOf({ 'package.json': JSON.stringify({ scripts: { test: 'jest', build: 'tsc' } }) }));
|
|
73
123
|
assert.equal(f.length, 0);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.202",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.202",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.202",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -827,8 +827,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
827
827
|
// landed since 1.19.198 with no explicit bump. run 32045249602. (task 1002620)
|
|
828
828
|
// 1.19.200 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
829
829
|
// landed since 1.19.199 with no explicit bump. run 32058296632. (task 1002620)
|
|
830
|
+
// 1.19.201 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
831
|
+
// landed since 1.19.200 with no explicit bump. run 32058666256. (task 1002620)
|
|
832
|
+
// 1.19.202 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
833
|
+
// landed since 1.19.201 with no explicit bump. run 32059276345. (task 1002620)
|
|
830
834
|
// ---------------------------------------------------------------------------
|
|
831
|
-
const CORE_VERSION = '1.19.
|
|
835
|
+
const CORE_VERSION = '1.19.202'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
832
836
|
|
|
833
837
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
834
838
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|