@codexstar/bug-hunter 3.0.0 → 3.0.5
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 +149 -83
- package/README.md +150 -15
- package/SKILL.md +94 -27
- package/agents/openai.yaml +4 -0
- package/bin/bug-hunter +9 -3
- package/docs/images/2026-03-12-fix-plan-rollout.png +0 -0
- package/docs/images/2026-03-12-hero-bug-hunter-overview.png +0 -0
- package/docs/images/2026-03-12-machine-readable-artifacts.png +0 -0
- package/docs/images/2026-03-12-pr-review-flow.png +0 -0
- package/docs/images/2026-03-12-security-pack.png +0 -0
- package/docs/images/adversarial-debate.png +0 -0
- package/docs/images/doc-verify-fix-plan.png +0 -0
- package/docs/images/hero.png +0 -0
- package/docs/images/pipeline-overview.png +0 -0
- package/docs/images/security-finding-card.png +0 -0
- package/docs/plans/2026-03-11-structured-output-migration-plan.md +288 -0
- package/docs/plans/2026-03-12-audit-bug-fixes-surgical-plan.md +193 -0
- package/docs/plans/2026-03-12-enterprise-security-pack-e2e-plan.md +59 -0
- package/docs/plans/2026-03-12-local-security-skills-integration-plan.md +39 -0
- package/docs/plans/2026-03-12-pr-review-strategic-fix-flow.md +78 -0
- package/evals/evals.json +366 -102
- package/modes/extended.md +2 -2
- package/modes/fix-loop.md +30 -30
- package/modes/fix-pipeline.md +32 -6
- package/modes/large-codebase.md +14 -15
- package/modes/local-sequential.md +44 -20
- package/modes/loop.md +56 -56
- package/modes/parallel.md +3 -3
- package/modes/scaled.md +2 -2
- package/modes/single-file.md +3 -3
- package/modes/small.md +11 -11
- package/package.json +10 -1
- package/prompts/fixer.md +37 -23
- package/prompts/hunter.md +39 -20
- package/prompts/referee.md +34 -20
- package/prompts/skeptic.md +25 -22
- package/schemas/coverage.schema.json +67 -0
- package/schemas/examples/findings.invalid.json +13 -0
- package/schemas/examples/findings.valid.json +17 -0
- package/schemas/findings.schema.json +76 -0
- package/schemas/fix-plan.schema.json +94 -0
- package/schemas/fix-report.schema.json +105 -0
- package/schemas/fix-strategy.schema.json +99 -0
- package/schemas/recon.schema.json +31 -0
- package/schemas/referee.schema.json +46 -0
- package/schemas/shared.schema.json +51 -0
- package/schemas/skeptic.schema.json +21 -0
- package/scripts/bug-hunter-state.cjs +35 -12
- package/scripts/code-index.cjs +11 -4
- package/scripts/fix-lock.cjs +95 -25
- package/scripts/payload-guard.cjs +24 -10
- package/scripts/pr-scope.cjs +181 -0
- package/scripts/render-report.cjs +346 -0
- package/scripts/run-bug-hunter.cjs +667 -32
- package/scripts/schema-runtime.cjs +273 -0
- package/scripts/schema-validate.cjs +40 -0
- package/scripts/tests/bug-hunter-state.test.cjs +68 -3
- package/scripts/tests/code-index.test.cjs +15 -0
- package/scripts/tests/fix-lock.test.cjs +60 -2
- package/scripts/tests/fixtures/flaky-worker.cjs +6 -1
- package/scripts/tests/fixtures/low-confidence-worker.cjs +8 -2
- package/scripts/tests/fixtures/success-worker.cjs +6 -1
- package/scripts/tests/payload-guard.test.cjs +154 -2
- package/scripts/tests/pr-scope.test.cjs +212 -0
- package/scripts/tests/render-report.test.cjs +180 -0
- package/scripts/tests/run-bug-hunter.test.cjs +686 -2
- package/scripts/tests/security-skills-integration.test.cjs +29 -0
- package/scripts/tests/skills-packaging.test.cjs +30 -0
- package/scripts/tests/worktree-harvest.test.cjs +66 -0
- package/scripts/worktree-harvest.cjs +62 -9
- package/skills/README.md +19 -0
- package/skills/commit-security-scan/SKILL.md +63 -0
- package/skills/security-review/SKILL.md +57 -0
- package/skills/threat-model-generation/SKILL.md +47 -0
- package/skills/vulnerability-validation/SKILL.md +59 -0
- package/templates/subagent-wrapper.md +12 -3
- package/modes/_dispatch.md +0 -121
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SCHEMA_FILES = {
|
|
5
|
+
recon: 'recon.schema.json',
|
|
6
|
+
findings: 'findings.schema.json',
|
|
7
|
+
skeptic: 'skeptic.schema.json',
|
|
8
|
+
referee: 'referee.schema.json',
|
|
9
|
+
coverage: 'coverage.schema.json',
|
|
10
|
+
'fix-report': 'fix-report.schema.json',
|
|
11
|
+
'fix-plan': 'fix-plan.schema.json',
|
|
12
|
+
'fix-strategy': 'fix-strategy.schema.json',
|
|
13
|
+
shared: 'shared.schema.json'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SCHEMA_CACHE = new Map();
|
|
17
|
+
|
|
18
|
+
function getSchemaDir() {
|
|
19
|
+
return path.resolve(__dirname, '..', 'schemas');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getKnownArtifacts() {
|
|
23
|
+
return Object.keys(SCHEMA_FILES).filter((name) => name !== 'shared');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getSchemaPath(artifactName) {
|
|
27
|
+
const fileName = SCHEMA_FILES[artifactName];
|
|
28
|
+
if (!fileName) {
|
|
29
|
+
throw new Error(`Unknown artifact schema: ${artifactName}`);
|
|
30
|
+
}
|
|
31
|
+
return path.join(getSchemaDir(), fileName);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadArtifactSchema(artifactName) {
|
|
35
|
+
if (!SCHEMA_FILES[artifactName]) {
|
|
36
|
+
throw new Error(`Unknown artifact schema: ${artifactName}`);
|
|
37
|
+
}
|
|
38
|
+
if (!SCHEMA_CACHE.has(artifactName)) {
|
|
39
|
+
const schemaPath = getSchemaPath(artifactName);
|
|
40
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
41
|
+
if (!Number.isInteger(schema.schemaVersion) || schema.schemaVersion <= 0) {
|
|
42
|
+
throw new Error(`Schema ${artifactName} is missing a valid schemaVersion`);
|
|
43
|
+
}
|
|
44
|
+
SCHEMA_CACHE.set(artifactName, { schema, schemaPath });
|
|
45
|
+
}
|
|
46
|
+
return SCHEMA_CACHE.get(artifactName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createSchemaRef(artifactName) {
|
|
50
|
+
const { schema, schemaPath } = loadArtifactSchema(artifactName);
|
|
51
|
+
return {
|
|
52
|
+
artifact: artifactName,
|
|
53
|
+
schemaVersion: schema.schemaVersion,
|
|
54
|
+
schemaFile: path.relative(path.resolve(__dirname, '..'), schemaPath)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateSchemaRef(reference) {
|
|
59
|
+
const errors = [];
|
|
60
|
+
if (!reference || typeof reference !== 'object' || Array.isArray(reference)) {
|
|
61
|
+
return { ok: false, errors: ['outputSchema must be an object'] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const artifactName = String(reference.artifact || '').trim();
|
|
65
|
+
if (!artifactName) {
|
|
66
|
+
errors.push('outputSchema.artifact must be a non-empty string');
|
|
67
|
+
} else if (!getKnownArtifacts().includes(artifactName)) {
|
|
68
|
+
errors.push(`outputSchema.artifact must be one of: ${getKnownArtifacts().join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!Number.isInteger(reference.schemaVersion) || reference.schemaVersion <= 0) {
|
|
72
|
+
errors.push('outputSchema.schemaVersion must be a positive integer');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (artifactName && getKnownArtifacts().includes(artifactName)) {
|
|
76
|
+
const { schema, schemaPath } = loadArtifactSchema(artifactName);
|
|
77
|
+
const expectedRelativePath = path.relative(path.resolve(__dirname, '..'), schemaPath);
|
|
78
|
+
if (reference.schemaVersion !== schema.schemaVersion) {
|
|
79
|
+
errors.push(`outputSchema.schemaVersion must match ${artifactName} schema version ${schema.schemaVersion}`);
|
|
80
|
+
}
|
|
81
|
+
if ('schemaFile' in reference && reference.schemaFile !== expectedRelativePath) {
|
|
82
|
+
errors.push(`outputSchema.schemaFile must match ${expectedRelativePath}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { ok: errors.length === 0, errors };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function describeType(value) {
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
return 'array';
|
|
92
|
+
}
|
|
93
|
+
if (value === null) {
|
|
94
|
+
return 'null';
|
|
95
|
+
}
|
|
96
|
+
return typeof value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveRef(rootSchema, ref) {
|
|
100
|
+
if (!ref.startsWith('#/')) {
|
|
101
|
+
throw new Error(`Unsupported schema ref: ${ref}`);
|
|
102
|
+
}
|
|
103
|
+
const parts = ref
|
|
104
|
+
.slice(2)
|
|
105
|
+
.split('/')
|
|
106
|
+
.map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
107
|
+
let current = rootSchema;
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
if (!current || typeof current !== 'object' || !(part in current)) {
|
|
110
|
+
throw new Error(`Unable to resolve schema ref: ${ref}`);
|
|
111
|
+
}
|
|
112
|
+
current = current[part];
|
|
113
|
+
}
|
|
114
|
+
return current;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateAgainstSchema({ value, schema, rootSchema, jsonPath, errors }) {
|
|
118
|
+
if (schema.$ref) {
|
|
119
|
+
const resolved = resolveRef(rootSchema, schema.$ref);
|
|
120
|
+
validateAgainstSchema({ value, schema: resolved, rootSchema, jsonPath, errors });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (schema.const !== undefined && value !== schema.const) {
|
|
125
|
+
errors.push(`${jsonPath} must equal ${JSON.stringify(schema.const)}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
130
|
+
errors.push(`${jsonPath} must be one of: ${schema.enum.join(', ')}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (schema.type === 'object') {
|
|
135
|
+
if (describeType(value) !== 'object') {
|
|
136
|
+
errors.push(`${jsonPath} must be an object`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const properties = schema.properties || {};
|
|
140
|
+
const required = schema.required || [];
|
|
141
|
+
for (const propertyName of required) {
|
|
142
|
+
if (!(propertyName in value)) {
|
|
143
|
+
errors.push(`${jsonPath}.${propertyName} is required`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const [propertyName, propertyValue] of Object.entries(value)) {
|
|
147
|
+
if (properties[propertyName]) {
|
|
148
|
+
validateAgainstSchema({
|
|
149
|
+
value: propertyValue,
|
|
150
|
+
schema: properties[propertyName],
|
|
151
|
+
rootSchema,
|
|
152
|
+
jsonPath: `${jsonPath}.${propertyName}`,
|
|
153
|
+
errors
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (schema.additionalProperties === false) {
|
|
158
|
+
errors.push(`${jsonPath}.${propertyName} is not allowed`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (schema.type === 'array') {
|
|
165
|
+
if (!Array.isArray(value)) {
|
|
166
|
+
errors.push(`${jsonPath} must be an array`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (Number.isInteger(schema.minItems) && value.length < schema.minItems) {
|
|
170
|
+
errors.push(`${jsonPath} must contain at least ${schema.minItems} item(s)`);
|
|
171
|
+
}
|
|
172
|
+
if (schema.items) {
|
|
173
|
+
value.forEach((item, index) => {
|
|
174
|
+
validateAgainstSchema({
|
|
175
|
+
value: item,
|
|
176
|
+
schema: schema.items,
|
|
177
|
+
rootSchema,
|
|
178
|
+
jsonPath: `${jsonPath}[${index}]`,
|
|
179
|
+
errors
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (schema.type === 'string') {
|
|
187
|
+
if (typeof value !== 'string') {
|
|
188
|
+
errors.push(`${jsonPath} must be a string`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (Number.isInteger(schema.minLength) && value.length < schema.minLength) {
|
|
192
|
+
errors.push(`${jsonPath} must not be empty`);
|
|
193
|
+
}
|
|
194
|
+
if (schema.pattern) {
|
|
195
|
+
const matcher = new RegExp(schema.pattern);
|
|
196
|
+
if (!matcher.test(value)) {
|
|
197
|
+
errors.push(`${jsonPath} must match ${schema.pattern}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (schema.type === 'number') {
|
|
204
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
205
|
+
errors.push(`${jsonPath} must be a number`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (typeof schema.minimum === 'number' && value < schema.minimum) {
|
|
209
|
+
errors.push(`${jsonPath} must be >= ${schema.minimum}`);
|
|
210
|
+
}
|
|
211
|
+
if (typeof schema.maximum === 'number' && value > schema.maximum) {
|
|
212
|
+
errors.push(`${jsonPath} must be <= ${schema.maximum}`);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (schema.type === 'integer') {
|
|
218
|
+
if (!Number.isInteger(value)) {
|
|
219
|
+
errors.push(`${jsonPath} must be an integer`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (typeof schema.minimum === 'number' && value < schema.minimum) {
|
|
223
|
+
errors.push(`${jsonPath} must be >= ${schema.minimum}`);
|
|
224
|
+
}
|
|
225
|
+
if (typeof schema.maximum === 'number' && value > schema.maximum) {
|
|
226
|
+
errors.push(`${jsonPath} must be <= ${schema.maximum}`);
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (schema.type === 'boolean' && typeof value !== 'boolean') {
|
|
232
|
+
errors.push(`${jsonPath} must be a boolean`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function validateArtifactValue({ artifactName, value }) {
|
|
237
|
+
const { schema, schemaPath } = loadArtifactSchema(artifactName);
|
|
238
|
+
const errors = [];
|
|
239
|
+
validateAgainstSchema({
|
|
240
|
+
value,
|
|
241
|
+
schema,
|
|
242
|
+
rootSchema: schema,
|
|
243
|
+
jsonPath: '$',
|
|
244
|
+
errors
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
ok: errors.length === 0,
|
|
248
|
+
artifact: artifactName,
|
|
249
|
+
schemaVersion: schema.schemaVersion,
|
|
250
|
+
schemaFile: path.relative(path.resolve(__dirname, '..'), schemaPath),
|
|
251
|
+
errors
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function validateArtifactFile({ artifactName, filePath }) {
|
|
256
|
+
try {
|
|
257
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
258
|
+
return validateArtifactValue({ artifactName, value });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
261
|
+
return { ok: false, artifact: artifactName, errors: [message] };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
createSchemaRef,
|
|
267
|
+
getKnownArtifacts,
|
|
268
|
+
getSchemaPath,
|
|
269
|
+
loadArtifactSchema,
|
|
270
|
+
validateArtifactFile,
|
|
271
|
+
validateArtifactValue,
|
|
272
|
+
validateSchemaRef
|
|
273
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
getKnownArtifacts,
|
|
7
|
+
validateArtifactFile
|
|
8
|
+
} = require('./schema-runtime.cjs');
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.error('Usage:');
|
|
12
|
+
console.error(' schema-validate.cjs <artifact-name> <file-path>');
|
|
13
|
+
console.error('');
|
|
14
|
+
console.error(`Artifacts: ${getKnownArtifacts().join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function main() {
|
|
18
|
+
const [artifactName, targetPath] = process.argv.slice(2);
|
|
19
|
+
if (!artifactName || !targetPath) {
|
|
20
|
+
usage();
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const result = validateArtifactFile({
|
|
25
|
+
artifactName,
|
|
26
|
+
filePath: path.resolve(targetPath)
|
|
27
|
+
});
|
|
28
|
+
console.log(JSON.stringify(result));
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
main();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
console.error(message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
@@ -49,8 +49,32 @@ test('bug-hunter-state init/mark/hash/filter/record works end-to-end', () => {
|
|
|
49
49
|
|
|
50
50
|
const findingsJson = path.join(sandbox, 'findings.json');
|
|
51
51
|
writeJson(findingsJson, [
|
|
52
|
-
{
|
|
53
|
-
|
|
52
|
+
{
|
|
53
|
+
bugId: 'BUG-1',
|
|
54
|
+
severity: 'Low',
|
|
55
|
+
category: 'logic',
|
|
56
|
+
file: 'src/x.ts',
|
|
57
|
+
lines: '1',
|
|
58
|
+
claim: 'x',
|
|
59
|
+
evidence: 'src/x.ts:1 first evidence',
|
|
60
|
+
runtimeTrigger: 'Call x()',
|
|
61
|
+
crossReferences: ['Single file'],
|
|
62
|
+
confidenceScore: 40
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
bugId: 'BUG-2',
|
|
66
|
+
severity: 'Critical',
|
|
67
|
+
category: 'security',
|
|
68
|
+
file: 'src/x.ts',
|
|
69
|
+
lines: '1',
|
|
70
|
+
claim: 'x',
|
|
71
|
+
evidence: 'src/x.ts:1 upgraded evidence',
|
|
72
|
+
runtimeTrigger: 'Call x() with attacker-controlled input',
|
|
73
|
+
crossReferences: ['Single file'],
|
|
74
|
+
confidenceScore: 95,
|
|
75
|
+
stride: 'Tampering',
|
|
76
|
+
cwe: 'CWE-20'
|
|
77
|
+
}
|
|
54
78
|
]);
|
|
55
79
|
const recorded = runJson('node', [stateScript, 'record-findings', statePath, findingsJson, 'test']);
|
|
56
80
|
assert.equal(recorded.inserted, 1);
|
|
@@ -64,7 +88,8 @@ test('bug-hunter-state init/mark/hash/filter/record works end-to-end', () => {
|
|
|
64
88
|
|
|
65
89
|
const state = readJson(statePath);
|
|
66
90
|
assert.equal(state.bugLedger[0].severity, 'Critical');
|
|
67
|
-
assert.equal(state.
|
|
91
|
+
assert.equal(state.bugLedger[0].confidenceScore, 95);
|
|
92
|
+
assert.equal(state.metrics.lowConfidenceFindings, 0);
|
|
68
93
|
|
|
69
94
|
const extraFile = path.join(sandbox, 'c.ts');
|
|
70
95
|
fs.writeFileSync(extraFile, 'const c = 3;\n', 'utf8');
|
|
@@ -85,3 +110,43 @@ test('bug-hunter-state init/mark/hash/filter/record works end-to-end', () => {
|
|
|
85
110
|
const updatedState = readJson(statePath);
|
|
86
111
|
assert.equal(updatedState.factCards['chunk-1'].apiContracts.length, 1);
|
|
87
112
|
});
|
|
113
|
+
|
|
114
|
+
test('bug-hunter-state rejects malformed findings artifacts', () => {
|
|
115
|
+
const sandbox = makeSandbox('bug-hunter-state-invalid-');
|
|
116
|
+
const stateScript = resolveSkillScript('bug-hunter-state.cjs');
|
|
117
|
+
const filePath = path.join(sandbox, 'a.ts');
|
|
118
|
+
fs.writeFileSync(filePath, 'const a = 1;\n', 'utf8');
|
|
119
|
+
|
|
120
|
+
const filesJson = path.join(sandbox, 'files.json');
|
|
121
|
+
writeJson(filesJson, [filePath]);
|
|
122
|
+
const statePath = path.join(sandbox, 'state.json');
|
|
123
|
+
runJson('node', [stateScript, 'init', statePath, 'extended', filesJson, '1']);
|
|
124
|
+
|
|
125
|
+
const findingsJson = path.join(sandbox, 'findings.json');
|
|
126
|
+
writeJson(findingsJson, [
|
|
127
|
+
{
|
|
128
|
+
bugId: 'BUG-1',
|
|
129
|
+
severity: 'Low',
|
|
130
|
+
category: 'logic',
|
|
131
|
+
file: 'src/x.ts',
|
|
132
|
+
lines: '1',
|
|
133
|
+
evidence: 'src/x.ts:1 evidence',
|
|
134
|
+
runtimeTrigger: 'Call x()',
|
|
135
|
+
crossReferences: ['Single file'],
|
|
136
|
+
confidenceScore: 40
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const result = require('node:child_process').spawnSync('node', [
|
|
141
|
+
stateScript,
|
|
142
|
+
'record-findings',
|
|
143
|
+
statePath,
|
|
144
|
+
findingsJson,
|
|
145
|
+
'test'
|
|
146
|
+
], {
|
|
147
|
+
encoding: 'utf8'
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
assert.notEqual(result.status, 0);
|
|
151
|
+
assert.match(`${result.stderr}${result.stdout}`, /Invalid findings artifact/);
|
|
152
|
+
});
|
|
@@ -55,3 +55,18 @@ test('code-index build captures symbols, call graph, boundaries, and query scope
|
|
|
55
55
|
assert.equal(queryResult.selected.includes(serviceFile), true);
|
|
56
56
|
assert.equal(queryResult.trustBoundaryFiles.includes(routeFile), true);
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
test('code-index query-bugs cleans up temp seed files after failures', () => {
|
|
60
|
+
const sandbox = makeSandbox('code-index-query-bugs-');
|
|
61
|
+
const codeIndex = resolveSkillScript('code-index.cjs');
|
|
62
|
+
const bugsJson = path.join(sandbox, 'bugs.json');
|
|
63
|
+
const missingIndexPath = path.join(sandbox, 'missing-index.json');
|
|
64
|
+
const sourceFile = path.join(sandbox, 'src', 'feature.ts');
|
|
65
|
+
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
|
|
66
|
+
fs.writeFileSync(sourceFile, 'export const feature = true;\n', 'utf8');
|
|
67
|
+
writeJson(bugsJson, [{ bugId: 'BUG-1', file: sourceFile }]);
|
|
68
|
+
|
|
69
|
+
const result = require('./test-utils.cjs').runRaw('node', [codeIndex, 'query-bugs', missingIndexPath, bugsJson, '1']);
|
|
70
|
+
assert.notEqual(result.status, 0);
|
|
71
|
+
assert.equal(fs.existsSync(path.join(sandbox, '.seed-files.tmp.json')), false);
|
|
72
|
+
});
|
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
runRaw
|
|
10
10
|
} = require('./test-utils.cjs');
|
|
11
11
|
|
|
12
|
-
test('fix-lock enforces single writer and supports release', () => {
|
|
12
|
+
test('fix-lock enforces single writer and supports token-protected release', () => {
|
|
13
13
|
const sandbox = makeSandbox('fix-lock-');
|
|
14
14
|
const lockScript = resolveSkillScript('fix-lock.cjs');
|
|
15
15
|
const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
|
|
@@ -17,20 +17,78 @@ test('fix-lock enforces single writer and supports release', () => {
|
|
|
17
17
|
const acquire1 = runJson('node', [lockScript, 'acquire', lockPath, '120']);
|
|
18
18
|
assert.equal(acquire1.ok, true);
|
|
19
19
|
assert.equal(acquire1.acquired, true);
|
|
20
|
+
assert.equal(typeof acquire1.lock.ownerToken, 'string');
|
|
21
|
+
assert.equal(acquire1.lock.ownerToken.length > 8, true);
|
|
20
22
|
|
|
21
23
|
const acquire2 = runRaw('node', [lockScript, 'acquire', lockPath, '120']);
|
|
22
24
|
assert.notEqual(acquire2.status, 0);
|
|
23
25
|
const output2 = `${acquire2.stdout || ''}${acquire2.stderr || ''}`;
|
|
24
26
|
assert.match(output2, /lock-held/);
|
|
25
27
|
|
|
28
|
+
const renew = runJson('node', [lockScript, 'renew', lockPath, acquire1.lock.ownerToken]);
|
|
29
|
+
assert.equal(renew.ok, true);
|
|
30
|
+
assert.equal(renew.renewed, true);
|
|
31
|
+
|
|
32
|
+
const badRelease = runRaw('node', [lockScript, 'release', lockPath, 'wrong-token']);
|
|
33
|
+
assert.notEqual(badRelease.status, 0);
|
|
34
|
+
assert.match(`${badRelease.stdout || ''}${badRelease.stderr || ''}`, /lock-owner-mismatch/);
|
|
35
|
+
|
|
26
36
|
const status = runJson('node', [lockScript, 'status', lockPath, '120']);
|
|
27
37
|
assert.equal(status.exists, true);
|
|
28
38
|
assert.equal(status.stale, false);
|
|
29
39
|
|
|
30
|
-
const release = runJson('node', [lockScript, 'release', lockPath]);
|
|
40
|
+
const release = runJson('node', [lockScript, 'release', lockPath, acquire1.lock.ownerToken]);
|
|
31
41
|
assert.equal(release.ok, true);
|
|
32
42
|
assert.equal(release.released, true);
|
|
33
43
|
|
|
34
44
|
const statusAfter = runJson('node', [lockScript, 'status', lockPath, '120']);
|
|
35
45
|
assert.equal(statusAfter.exists, false);
|
|
36
46
|
});
|
|
47
|
+
|
|
48
|
+
test('fix-lock does not steal an expired lock from a still-running owner', () => {
|
|
49
|
+
const sandbox = makeSandbox('fix-lock-live-owner-');
|
|
50
|
+
const lockScript = resolveSkillScript('fix-lock.cjs');
|
|
51
|
+
const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
|
|
52
|
+
|
|
53
|
+
require('fs').writeFileSync(lockPath, `${JSON.stringify({
|
|
54
|
+
pid: process.pid,
|
|
55
|
+
host: 'test-host',
|
|
56
|
+
cwd: sandbox,
|
|
57
|
+
createdAtMs: Date.now() - 10_000,
|
|
58
|
+
createdAt: new Date(Date.now() - 10_000).toISOString(),
|
|
59
|
+
ownerToken: 'existing-owner-token'
|
|
60
|
+
}, null, 2)}\n`, 'utf8');
|
|
61
|
+
|
|
62
|
+
const acquire = runRaw('node', [lockScript, 'acquire', lockPath, '1']);
|
|
63
|
+
assert.notEqual(acquire.status, 0);
|
|
64
|
+
assert.match(`${acquire.stdout || ''}${acquire.stderr || ''}`, /lock-held-by-live-owner|lock-held/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('fix-lock acquires atomically under contention', async () => {
|
|
68
|
+
const sandbox = makeSandbox('fix-lock-race-');
|
|
69
|
+
const lockScript = resolveSkillScript('fix-lock.cjs');
|
|
70
|
+
const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
|
|
71
|
+
|
|
72
|
+
const results = await Promise.all(Array.from({ length: 20 }, () => {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const child = require('node:child_process').spawn('node', [lockScript, 'acquire', lockPath, '120'], {
|
|
75
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
76
|
+
});
|
|
77
|
+
child.on('close', (code) => resolve(code));
|
|
78
|
+
});
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const successCount = results.filter((code) => code === 0).length;
|
|
82
|
+
assert.equal(successCount, 1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('fix-lock recovers from a corrupted lock file', () => {
|
|
86
|
+
const sandbox = makeSandbox('fix-lock-corrupt-');
|
|
87
|
+
const lockScript = resolveSkillScript('fix-lock.cjs');
|
|
88
|
+
const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
|
|
89
|
+
require('fs').writeFileSync(lockPath, '{broken json', 'utf8');
|
|
90
|
+
|
|
91
|
+
const result = runJson('node', [lockScript, 'acquire', lockPath, '120']);
|
|
92
|
+
assert.equal(result.ok, true);
|
|
93
|
+
assert.equal(result.acquired, true);
|
|
94
|
+
});
|
|
@@ -54,9 +54,14 @@ if (findingsJson) {
|
|
|
54
54
|
{
|
|
55
55
|
bugId: `BUG-${chunkId}`,
|
|
56
56
|
severity: 'Medium',
|
|
57
|
+
category: 'logic',
|
|
57
58
|
file: `src/retry-${chunkId}.ts`,
|
|
58
59
|
lines: '10-11',
|
|
59
|
-
claim: `retry-success-${chunkId}
|
|
60
|
+
claim: `retry-success-${chunkId}`,
|
|
61
|
+
evidence: `src/retry-${chunkId}.ts:10-11 retry success evidence`,
|
|
62
|
+
runtimeTrigger: `Retry attempt for ${chunkId}`,
|
|
63
|
+
crossReferences: ['Single file'],
|
|
64
|
+
confidenceScore: 88
|
|
60
65
|
}
|
|
61
66
|
];
|
|
62
67
|
fs.writeFileSync(findingsJson, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
@@ -54,10 +54,16 @@ if (findingsJson && scanFiles.length > 0) {
|
|
|
54
54
|
{
|
|
55
55
|
bugId: `BUG-${chunkId}`,
|
|
56
56
|
severity: 'Critical',
|
|
57
|
-
|
|
57
|
+
category: 'security',
|
|
58
58
|
file: scanFiles[0],
|
|
59
59
|
lines: '1',
|
|
60
|
-
claim: `Low-confidence risk in ${path.basename(scanFiles[0])}
|
|
60
|
+
claim: `Low-confidence risk in ${path.basename(scanFiles[0])}`,
|
|
61
|
+
evidence: `${scanFiles[0]}:1 fixture evidence`,
|
|
62
|
+
runtimeTrigger: `Load ${path.basename(scanFiles[0])} through the low-confidence worker`,
|
|
63
|
+
crossReferences: ['Single file'],
|
|
64
|
+
confidenceScore: Number.isInteger(confidence) ? confidence : 60,
|
|
65
|
+
stride: 'Tampering',
|
|
66
|
+
cwe: 'CWE-20'
|
|
61
67
|
}
|
|
62
68
|
]);
|
|
63
69
|
}
|
|
@@ -34,9 +34,14 @@ const payload = [
|
|
|
34
34
|
{
|
|
35
35
|
bugId: `BUG-${options['chunk-id'] || '0'}`,
|
|
36
36
|
severity: 'Low',
|
|
37
|
+
category: 'logic',
|
|
37
38
|
file: 'src/example.ts',
|
|
38
39
|
lines: '1',
|
|
39
|
-
claim: 'example'
|
|
40
|
+
claim: 'example',
|
|
41
|
+
evidence: 'src/example.ts:1 example evidence',
|
|
42
|
+
runtimeTrigger: 'Run the success worker fixture',
|
|
43
|
+
crossReferences: ['Single file'],
|
|
44
|
+
confidenceScore: 80
|
|
40
45
|
}
|
|
41
46
|
];
|
|
42
47
|
fs.writeFileSync(findingsJson, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|