@oneie/claude 0.8.0 → 0.10.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/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- package/templates/template-todo.md +29 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
const test = require("node:test");
|
|
7
|
+
const schema = require("./report-schema.json");
|
|
8
|
+
const {
|
|
9
|
+
LIMITS,
|
|
10
|
+
collect,
|
|
11
|
+
collectSchemaErrors,
|
|
12
|
+
validateDocument,
|
|
13
|
+
} = require("./validate-findings.cjs");
|
|
14
|
+
|
|
15
|
+
const validatorPath = path.join(__dirname, "validate-findings.cjs");
|
|
16
|
+
const CLI_TIMEOUT_MS = 5000;
|
|
17
|
+
const HOSTILE_CLI_TIMEOUT_MS = 15000;
|
|
18
|
+
const HAS_SAFE_INPUT_OPEN = Number.isInteger(fs.constants.O_NOFOLLOW) &&
|
|
19
|
+
fs.constants.O_NOFOLLOW !== 0 &&
|
|
20
|
+
Number.isInteger(fs.constants.O_NONBLOCK) &&
|
|
21
|
+
fs.constants.O_NONBLOCK !== 0;
|
|
22
|
+
const TERMINAL_CONTROL_PAYLOAD = "\u001b\u0007\u0085\u202e\u034f\ufe0f";
|
|
23
|
+
const TERMINAL_CONTROL_BYTES = [
|
|
24
|
+
Buffer.from([0x1b]),
|
|
25
|
+
Buffer.from([0x07]),
|
|
26
|
+
Buffer.from("\u0085"),
|
|
27
|
+
Buffer.from("\u202e"),
|
|
28
|
+
Buffer.from("\u034f"),
|
|
29
|
+
Buffer.from("\ufe0f"),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function source(kind = "entrypoint", file = "src/handler.c", line = 10) {
|
|
33
|
+
return { kind, file, line, scope: "handle", description: "Attacker data reaches the operation." };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function evidence(file = "src/handler.c", line = 10) {
|
|
37
|
+
return { file, line, description: "The source performs the operation without the required check." };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function confirmed() {
|
|
41
|
+
return {
|
|
42
|
+
verdict: "confirmed",
|
|
43
|
+
fingerprint: "src-handler-missing-check",
|
|
44
|
+
title: "Missing ownership check",
|
|
45
|
+
description: "An attacker can reach an operation without the intended ownership check.",
|
|
46
|
+
root_cause: "handle omits the ownership check before changing the object.",
|
|
47
|
+
intended_behavior: "Only the object's owner can change it.",
|
|
48
|
+
trace: [source("entrypoint"), source("propagation", "src/model.c", 20), source("sink", "src/store.c", 30)],
|
|
49
|
+
evidence: [evidence()],
|
|
50
|
+
conditions: [],
|
|
51
|
+
execution: {
|
|
52
|
+
attacker_perspective: "An unprivileged remote user with their own account.",
|
|
53
|
+
payloads: ["An object identifier owned by another user."],
|
|
54
|
+
instructions: ["Submit the identifier through the public operation."],
|
|
55
|
+
observed_result: "The other user's object changes.",
|
|
56
|
+
},
|
|
57
|
+
remediation: { strategy: "Check ownership before the state change." },
|
|
58
|
+
severity: {
|
|
59
|
+
likelihood: { score: "medium", reason: "The operation is directly reachable." },
|
|
60
|
+
impact: { score: "medium", reason: "The attacker changes one protected object." },
|
|
61
|
+
overall_severity: "medium",
|
|
62
|
+
},
|
|
63
|
+
confidence: { score: "high", reason: "The source path and result were reproduced." },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function needsValidation() {
|
|
68
|
+
return {
|
|
69
|
+
verdict: "needs_validation",
|
|
70
|
+
fingerprint: "src-parser-size-hypothesis",
|
|
71
|
+
title: "Unchecked parsed size",
|
|
72
|
+
description: "A parsed size may reach an allocation without a limit.",
|
|
73
|
+
claimed_root_cause: "parse_size may pass an unbounded value to allocate.",
|
|
74
|
+
trace: [source()],
|
|
75
|
+
evidence: [evidence()],
|
|
76
|
+
blockers: ["The generated parser source is absent from this checkout."],
|
|
77
|
+
validation_plan: {
|
|
78
|
+
local: "Generate the parser and submit the smallest input that exceeds the documented limit.",
|
|
79
|
+
deployment: "In an approved test deployment, confirm the request reaches the generated parser and record the bounded observable result.",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rejected() {
|
|
85
|
+
return {
|
|
86
|
+
verdict: "rejected",
|
|
87
|
+
fingerprint: "src-router-auth-bypass",
|
|
88
|
+
title: "Authorization bypass in router",
|
|
89
|
+
description: "The candidate claimed a route bypassed authorization.",
|
|
90
|
+
claimed_root_cause: "dispatch was claimed to skip the authorization wrapper.",
|
|
91
|
+
trace: [source("sink")],
|
|
92
|
+
evidence: [evidence()],
|
|
93
|
+
reason: "All routes pass through the authorization wrapper before dispatch.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function errorsFor(value) {
|
|
98
|
+
return validateDocument(value, schema);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runCli(contents, options = {}) {
|
|
102
|
+
const { nodeArgs = [], timeout = CLI_TIMEOUT_MS } = options;
|
|
103
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "validate-findings-"));
|
|
104
|
+
const findingsPath = path.join(directory, "findings.json");
|
|
105
|
+
try {
|
|
106
|
+
fs.writeFileSync(findingsPath, contents);
|
|
107
|
+
return spawnSync(process.execPath, [...nodeArgs, validatorPath, findingsPath], {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
timeout,
|
|
110
|
+
});
|
|
111
|
+
} finally {
|
|
112
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cliOutput(result) {
|
|
117
|
+
return `${result.stdout}${result.stderr}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertNoInjectedControlBytes(output) {
|
|
121
|
+
const bytes = Buffer.isBuffer(output) ? output : Buffer.from(output, "utf8");
|
|
122
|
+
for (const marker of TERMINAL_CONTROL_BYTES) {
|
|
123
|
+
assert.equal(bytes.indexOf(marker), -1, `found raw control bytes ${marker.toString("hex")}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function producerShapedFindings() {
|
|
128
|
+
const demonstrated = confirmed();
|
|
129
|
+
demonstrated.conditions = [{
|
|
130
|
+
kind: "authentication_level",
|
|
131
|
+
description: "The attacker needs a normal account.",
|
|
132
|
+
}];
|
|
133
|
+
demonstrated.execution.payloads = ["", " \t\r\n", "\u0000\u001f\u007f", "\u034f", "\ufe0f", "\ud800", "\udc00", "[{,}]\\\""];
|
|
134
|
+
demonstrated.remediation.code_changes = [{
|
|
135
|
+
file_name: "src/handler.c",
|
|
136
|
+
fixed_code: "",
|
|
137
|
+
}];
|
|
138
|
+
|
|
139
|
+
const blocked = needsValidation();
|
|
140
|
+
delete blocked.validation_plan.deployment;
|
|
141
|
+
|
|
142
|
+
return [demonstrated, blocked, rejected()];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function rejectMutation(factory, mutate) {
|
|
146
|
+
const value = factory();
|
|
147
|
+
mutate(value);
|
|
148
|
+
assert.notEqual(errorsFor([value]).length, 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
test("schema is an actual top-level array with exactly three branches", () => {
|
|
152
|
+
assert.equal(schema.type, "array");
|
|
153
|
+
assert.equal(schema.items.oneOf.length, 3);
|
|
154
|
+
assert.deepEqual(schema.items.oneOf.map((branch) => branch.properties.verdict.const), [
|
|
155
|
+
"confirmed", "needs_validation", "rejected",
|
|
156
|
+
]);
|
|
157
|
+
const confirmedSchema = schema.items.oneOf[0].properties;
|
|
158
|
+
assert.equal(confirmedSchema.title.visibleContent, true);
|
|
159
|
+
assert.equal(confirmedSchema.execution.properties.payloads.items.minLength, undefined);
|
|
160
|
+
assert.equal(confirmedSchema.execution.properties.payloads.items.visibleContent, undefined);
|
|
161
|
+
assert.equal(confirmedSchema.remediation.properties.code_changes.items.properties.fixed_code.minLength, undefined);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("accepts a producer-shaped findings document through the CLI", () => {
|
|
165
|
+
const result = runCli(JSON.stringify(producerShapedFindings()));
|
|
166
|
+
assert.equal(result.status, 0, cliOutput(result));
|
|
167
|
+
assert.match(result.stdout, /PASS: 3 findings valid/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("accepts empty output and each complete branch", () => {
|
|
171
|
+
assert.deepEqual(errorsFor([]), []);
|
|
172
|
+
assert.deepEqual(errorsFor([confirmed(), needsValidation(), rejected()]), []);
|
|
173
|
+
const localOnly = needsValidation();
|
|
174
|
+
delete localOnly.validation_plan.deployment;
|
|
175
|
+
assert.deepEqual(errorsFor([localOnly]), []);
|
|
176
|
+
const deploymentOnly = needsValidation();
|
|
177
|
+
delete deploymentOnly.validation_plan.local;
|
|
178
|
+
assert.deepEqual(errorsFor([deploymentOnly]), []);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("allows a one-line finding trace", () => {
|
|
182
|
+
const finding = confirmed();
|
|
183
|
+
finding.trace = [source("entrypoint")];
|
|
184
|
+
assert.deepEqual(errorsFor([finding]), []);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("rejects empty required content", () => {
|
|
188
|
+
const cases = [
|
|
189
|
+
[confirmed, (finding) => { finding.title = ""; }],
|
|
190
|
+
[confirmed, (finding) => { finding.title = " "; }],
|
|
191
|
+
[confirmed, (finding) => { finding.evidence = []; }],
|
|
192
|
+
[confirmed, (finding) => { finding.execution.payloads = []; }],
|
|
193
|
+
[confirmed, (finding) => { finding.execution.instructions = []; }],
|
|
194
|
+
[confirmed, (finding) => { finding.execution.observed_result = ""; }],
|
|
195
|
+
[confirmed, (finding) => { finding.remediation.strategy = ""; }],
|
|
196
|
+
[needsValidation, (finding) => { finding.blockers = []; }],
|
|
197
|
+
[needsValidation, (finding) => { finding.validation_plan = {}; }],
|
|
198
|
+
[needsValidation, (finding) => { finding.validation_plan = { local: " " }; }],
|
|
199
|
+
[rejected, (finding) => { finding.claimed_root_cause = ""; }],
|
|
200
|
+
];
|
|
201
|
+
for (const [factory, mutate] of cases) rejectMutation(factory, mutate);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("preserves exact payload and replacement-code strings", () => {
|
|
205
|
+
const finding = confirmed();
|
|
206
|
+
const payloads = ["", " \t\r\n", "\u0000\u001f\u007f", "\u034f", "\ufe0f", "\ud800", "\udc00"];
|
|
207
|
+
const fixedCode = "\u0000 \t\r\n\u001f\u007f\u034f\ufe0f\ud800x\udc00";
|
|
208
|
+
finding.execution.payloads = payloads.slice();
|
|
209
|
+
finding.remediation.code_changes = [{ file_name: "src/handler.c", fixed_code: fixedCode }];
|
|
210
|
+
|
|
211
|
+
assert.deepEqual(errorsFor([finding]), []);
|
|
212
|
+
assert.deepEqual(finding.execution.payloads, payloads);
|
|
213
|
+
assert.equal(finding.remediation.code_changes[0].fixed_code, fixedCode);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("rejects invalid scalars and whitespace, control, format, or default-ignorable prose", () => {
|
|
217
|
+
for (const invisible of ["\u0000\t\r\n\u001f\u007f\u200b", "\u034f", "\ufe0f", "\ud800", "\udc00", "visible\ud800"]) {
|
|
218
|
+
const cases = [
|
|
219
|
+
[confirmed, (finding) => { finding.title = invisible; }],
|
|
220
|
+
[confirmed, (finding) => { finding.trace[0].scope = invisible; }],
|
|
221
|
+
[confirmed, (finding) => { finding.evidence[0].description = invisible; }],
|
|
222
|
+
[confirmed, (finding) => { finding.execution.instructions = [invisible]; }],
|
|
223
|
+
[confirmed, (finding) => { finding.remediation.strategy = invisible; }],
|
|
224
|
+
[confirmed, (finding) => { finding.severity.impact.reason = invisible; }],
|
|
225
|
+
[confirmed, (finding) => { finding.confidence.reason = invisible; }],
|
|
226
|
+
[needsValidation, (finding) => { finding.blockers = [invisible]; }],
|
|
227
|
+
[needsValidation, (finding) => { finding.validation_plan = { local: invisible }; }],
|
|
228
|
+
[rejected, (finding) => { finding.reason = invisible; }],
|
|
229
|
+
];
|
|
230
|
+
for (const [factory, mutate] of cases) rejectMutation(factory, mutate);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("quotes input-derived controls in direct validation values and paths", () => {
|
|
235
|
+
const finding = confirmed();
|
|
236
|
+
finding.trace[0].kind = `invalid-${TERMINAL_CONTROL_PAYLOAD}`;
|
|
237
|
+
finding.execution[`extra-${TERMINAL_CONTROL_PAYLOAD}`] = "value";
|
|
238
|
+
|
|
239
|
+
const cyclic = {};
|
|
240
|
+
cyclic[`path-${TERMINAL_CONTROL_PAYLOAD}`] = cyclic;
|
|
241
|
+
const output = [
|
|
242
|
+
...errorsFor([finding]),
|
|
243
|
+
...collect(cyclic, { type: "object" }, "$input"),
|
|
244
|
+
].join("\n");
|
|
245
|
+
|
|
246
|
+
for (const escaped of ["\\u001b", "\\u0007", "\\u0085", "\\u202e", "\\u034f", "\\ufe0f"]) {
|
|
247
|
+
assert(output.includes(escaped), `missing escaped diagnostic ${escaped}`);
|
|
248
|
+
}
|
|
249
|
+
assertNoInjectedControlBytes(output);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("rejects line zero", () => {
|
|
253
|
+
rejectMutation(confirmed, (finding) => { finding.trace[0].line = 0; });
|
|
254
|
+
rejectMutation(rejected, (finding) => { finding.evidence[0].line = 0; });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("does not treat inherited or Object-prototype properties as schema properties", () => {
|
|
258
|
+
rejectMutation(confirmed, (finding) => { finding.constructor = "not allowed"; });
|
|
259
|
+
const inherited = Object.create({ verdict: "confirmed" });
|
|
260
|
+
assert(errorsFor([inherited]).some((error) => error.includes("exactly one")));
|
|
261
|
+
assert(collect(Object.create({ constructor: "inherited" }), {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: { constructor: { type: "string" } },
|
|
264
|
+
required: ["constructor"],
|
|
265
|
+
additionalProperties: false,
|
|
266
|
+
}).some((error) => error.includes("missing required")));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("oneOf requires exactly one passing branch", () => {
|
|
270
|
+
assert(collect("value", { oneOf: [{ type: "string" }, { minLength: 1 }] }, "$test")
|
|
271
|
+
.some((error) => error.includes("matched 2")));
|
|
272
|
+
assert(collect(7, { oneOf: [{ type: "string" }, { minimum: 10 }] }, "$test")
|
|
273
|
+
.some((error) => error.includes("matched 0")));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("rejects duplicate fingerprints and unique array entries", () => {
|
|
277
|
+
const first = confirmed();
|
|
278
|
+
const second = rejected();
|
|
279
|
+
second.fingerprint = first.fingerprint;
|
|
280
|
+
assert(errorsFor([first, second]).some((error) => error.includes("duplicate of")));
|
|
281
|
+
rejectMutation(needsValidation, (finding) => { finding.blockers = [finding.blockers[0], finding.blockers[0]]; });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("uses canonical Set uniqueness for structured entries at the array limit", () => {
|
|
285
|
+
const entries = Array.from({ length: LIMITS.arrayItems }, (_, id) => ({ id, label: String(id) }));
|
|
286
|
+
assert.deepEqual(collect(entries, { type: "array", uniqueItems: true }), []);
|
|
287
|
+
|
|
288
|
+
const duplicate = entries.slice(0, -1);
|
|
289
|
+
duplicate.push({ label: "0", id: 0 });
|
|
290
|
+
assert(collect(duplicate, { type: "array", uniqueItems: true })
|
|
291
|
+
.some((error) => error.includes(`duplicate at index ${LIMITS.arrayItems - 1}`)));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("bounds canonical uniqueness keys and Set storage", () => {
|
|
295
|
+
const oversizedKey = "x".repeat(LIMITS.canonicalKeyBytes + 1);
|
|
296
|
+
assert(collect([oversizedKey], { type: "array", uniqueItems: true })
|
|
297
|
+
.some((error) => error.includes("canonical key exceeds")));
|
|
298
|
+
|
|
299
|
+
const itemLength = Math.floor(LIMITS.uniqueSetBytes / 6);
|
|
300
|
+
const largeUniqueItems = Array.from({ length: 6 }, (_, index) => `${index}${"x".repeat(itemLength)}`);
|
|
301
|
+
assert(collect(largeUniqueItems, { type: "array", uniqueItems: true })
|
|
302
|
+
.some((error) => error.includes("canonical uniqueness set exceeds")));
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("requires findings to be sorted by fingerprint", () => {
|
|
306
|
+
const first = confirmed();
|
|
307
|
+
const second = rejected();
|
|
308
|
+
assert(errorsFor([second, first]).some((error) => error.includes("sorted lexicographically")));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("rejects severity above demonstrated impact", () => {
|
|
312
|
+
rejectMutation(confirmed, (finding) => {
|
|
313
|
+
finding.severity.overall_severity = "high";
|
|
314
|
+
finding.severity.impact.score = "medium";
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("rejects unsafe source paths", () => {
|
|
319
|
+
const badPaths = [
|
|
320
|
+
"/etc/passwd",
|
|
321
|
+
"../src/file.c",
|
|
322
|
+
"src/../file.c",
|
|
323
|
+
"src//file.c",
|
|
324
|
+
"C:\\src\\file.c",
|
|
325
|
+
"src/file:name.c",
|
|
326
|
+
"src/file\nname.c",
|
|
327
|
+
"src/file\u0001name.c",
|
|
328
|
+
"src/file\u0085name.c",
|
|
329
|
+
"src/file\u2028name.c",
|
|
330
|
+
"src/file\u202ename.c",
|
|
331
|
+
"src/file\u2066name.c",
|
|
332
|
+
"src/file\u200dname.c",
|
|
333
|
+
"src/file\u034fname.c",
|
|
334
|
+
"src/file\ufe0fname.c",
|
|
335
|
+
"src/file\ud800name.c",
|
|
336
|
+
"src/file\udc00name.c",
|
|
337
|
+
"CON",
|
|
338
|
+
"src/con.txt",
|
|
339
|
+
"src/PRN",
|
|
340
|
+
"src/AUX.c",
|
|
341
|
+
"src/NUL",
|
|
342
|
+
"src/COM1.log",
|
|
343
|
+
"src/lpt9",
|
|
344
|
+
"src/CONIN$",
|
|
345
|
+
"src/CONOUT$.txt",
|
|
346
|
+
"src/CLOCK$.txt",
|
|
347
|
+
"src/COM\u00b9.log",
|
|
348
|
+
"src/LPT\u00b2.log",
|
|
349
|
+
"src /file.c",
|
|
350
|
+
"src./file.c",
|
|
351
|
+
"src/file.c ",
|
|
352
|
+
"src/file.c.",
|
|
353
|
+
];
|
|
354
|
+
for (const badPath of badPaths) {
|
|
355
|
+
rejectMutation(confirmed, (finding) => { finding.trace[0].file = badPath; });
|
|
356
|
+
}
|
|
357
|
+
rejectMutation(rejected, (finding) => { finding.evidence[0].file = "NUL.txt"; });
|
|
358
|
+
rejectMutation(confirmed, (finding) => {
|
|
359
|
+
finding.remediation.code_changes = [{ file_name: "src/file:name.c", fixed_code: "replacement" }];
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("accepts legitimate Unicode source paths and prose", () => {
|
|
364
|
+
const finding = confirmed();
|
|
365
|
+
finding.title = "Finding \ud83d\ude00 cafe\u0301";
|
|
366
|
+
finding.trace[0].file = "src/日本語/cafe\u0301-\ud83d\ude00.ts";
|
|
367
|
+
finding.evidence[0].file = "src/mañana/файл.ts";
|
|
368
|
+
finding.remediation.code_changes = [{
|
|
369
|
+
file_name: "src/修正/éxito.ts",
|
|
370
|
+
fixed_code: "replacement",
|
|
371
|
+
}];
|
|
372
|
+
assert.deepEqual(errorsFor([finding]), []);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("CLI rejects input above the byte limit without an exception trace", () => {
|
|
376
|
+
const result = runCli(Buffer.alloc(LIMITS.inputBytes + 1, 0x20));
|
|
377
|
+
const output = cliOutput(result);
|
|
378
|
+
assert.equal(result.status, 1, output);
|
|
379
|
+
assert.match(output, new RegExp(`input exceeds ${LIMITS.inputBytes} byte limit`));
|
|
380
|
+
assert.doesNotMatch(output, /RangeError|Maximum call stack|heap out of memory/i);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("CLI rejects invalid UTF-8 without replacement or an exception trace", () => {
|
|
384
|
+
const findings = producerShapedFindings();
|
|
385
|
+
findings[0].execution.payloads = ["INVALID_UTF8"];
|
|
386
|
+
const encoded = Buffer.from(JSON.stringify(findings));
|
|
387
|
+
const marker = Buffer.from("INVALID_UTF8");
|
|
388
|
+
const markerOffset = encoded.indexOf(marker);
|
|
389
|
+
assert.notEqual(markerOffset, -1);
|
|
390
|
+
const malformed = Buffer.concat([
|
|
391
|
+
encoded.subarray(0, markerOffset),
|
|
392
|
+
Buffer.from([0x80]),
|
|
393
|
+
encoded.subarray(markerOffset + marker.length),
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
const result = runCli(malformed);
|
|
397
|
+
const output = cliOutput(result);
|
|
398
|
+
assert.equal(result.status, 1, output);
|
|
399
|
+
assert.match(output, /input is not valid UTF-8/);
|
|
400
|
+
assert.doesNotMatch(output, /TypeError|stack|at validate-findings/i);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("quotes input-derived controls in CLI validation errors", { skip: !HAS_SAFE_INPUT_OPEN }, () => {
|
|
404
|
+
const finding = confirmed();
|
|
405
|
+
finding.trace[0].kind = `invalid-${TERMINAL_CONTROL_PAYLOAD}`;
|
|
406
|
+
finding.execution[`extra-${TERMINAL_CONTROL_PAYLOAD}`] = "value";
|
|
407
|
+
const result = runCli(JSON.stringify([finding]));
|
|
408
|
+
|
|
409
|
+
assert.equal(result.status, 1, cliOutput(result));
|
|
410
|
+
assert.match(result.stderr, /\$\[0\]\.trace\[0\]\.kind/);
|
|
411
|
+
assert.match(result.stderr, /\\u001b/);
|
|
412
|
+
assert.match(result.stderr, /\\u202e/);
|
|
413
|
+
assertNoInjectedControlBytes(result.stderr);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("returns a generic syntax error without parser-supplied controls", { skip: !HAS_SAFE_INPUT_OPEN }, () => {
|
|
417
|
+
const malformed = Buffer.concat([
|
|
418
|
+
Buffer.from("["),
|
|
419
|
+
Buffer.from(TERMINAL_CONTROL_PAYLOAD),
|
|
420
|
+
Buffer.from("]"),
|
|
421
|
+
]);
|
|
422
|
+
const result = runCli(malformed);
|
|
423
|
+
|
|
424
|
+
assert.equal(result.status, 1, cliOutput(result));
|
|
425
|
+
assert.equal(result.stderr, "Failed to parse findings JSON: invalid JSON syntax\n");
|
|
426
|
+
assertNoInjectedControlBytes(result.stderr);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("does not reflect controls from a failed CLI input path", () => {
|
|
430
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "validate-findings-path-"));
|
|
431
|
+
const missingPath = path.join(directory, `missing-${TERMINAL_CONTROL_PAYLOAD}.json`);
|
|
432
|
+
try {
|
|
433
|
+
const result = spawnSync(process.execPath, [validatorPath, missingPath], {
|
|
434
|
+
encoding: "utf8",
|
|
435
|
+
timeout: CLI_TIMEOUT_MS,
|
|
436
|
+
});
|
|
437
|
+
assert.equal(result.status, 1, cliOutput(result));
|
|
438
|
+
assert.match(result.stderr, /Failed to read findings JSON:/);
|
|
439
|
+
assertNoInjectedControlBytes(result.stderr);
|
|
440
|
+
} finally {
|
|
441
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("CLI rejects lone-surrogate prose without changing payload semantics", () => {
|
|
446
|
+
const findings = producerShapedFindings();
|
|
447
|
+
findings[0].title = "\ud800";
|
|
448
|
+
const result = runCli(JSON.stringify(findings));
|
|
449
|
+
const output = cliOutput(result);
|
|
450
|
+
assert.equal(result.status, 1, output);
|
|
451
|
+
assert.match(output, /must contain only valid Unicode scalar values/);
|
|
452
|
+
assert.doesNotMatch(output, /stack|at validate-findings/i);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("CLI rejects Unicode format controls in source paths", () => {
|
|
456
|
+
const findings = producerShapedFindings();
|
|
457
|
+
findings[0].trace[0].file = "src/file\u202ename.c";
|
|
458
|
+
const result = runCli(JSON.stringify(findings));
|
|
459
|
+
const output = cliOutput(result);
|
|
460
|
+
assert.equal(result.status, 1, output);
|
|
461
|
+
assert.match(output, /must be a safe repository-relative source path/);
|
|
462
|
+
assert.doesNotMatch(output, /stack|at validate-findings/i);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("CLI rejects a FIFO without blocking", { skip: process.platform === "win32" || !HAS_SAFE_INPUT_OPEN }, () => {
|
|
466
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "validate-findings-fifo-"));
|
|
467
|
+
const fifoPath = path.join(directory, "findings.json");
|
|
468
|
+
try {
|
|
469
|
+
const created = spawnSync("mkfifo", [fifoPath], { encoding: "utf8", timeout: CLI_TIMEOUT_MS });
|
|
470
|
+
assert.equal(created.status, 0, cliOutput(created));
|
|
471
|
+
|
|
472
|
+
const result = spawnSync(process.execPath, [validatorPath, fifoPath], {
|
|
473
|
+
encoding: "utf8",
|
|
474
|
+
timeout: CLI_TIMEOUT_MS,
|
|
475
|
+
});
|
|
476
|
+
const output = cliOutput(result);
|
|
477
|
+
assert.notEqual(result.error && result.error.code, "ETIMEDOUT", output);
|
|
478
|
+
assert.equal(result.status, 1, output);
|
|
479
|
+
assert.match(output, /input must be a regular file/);
|
|
480
|
+
assert.doesNotMatch(output, /stack|at validate-findings/i);
|
|
481
|
+
} finally {
|
|
482
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("CLI rejects a symlink without following it", { skip: process.platform === "win32" || !HAS_SAFE_INPUT_OPEN }, () => {
|
|
487
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "validate-findings-symlink-"));
|
|
488
|
+
const targetPath = path.join(directory, "target.json");
|
|
489
|
+
const symlinkPath = path.join(directory, "findings.json");
|
|
490
|
+
try {
|
|
491
|
+
fs.writeFileSync(targetPath, JSON.stringify(producerShapedFindings()));
|
|
492
|
+
fs.symlinkSync(targetPath, symlinkPath);
|
|
493
|
+
const result = spawnSync(process.execPath, [validatorPath, symlinkPath], {
|
|
494
|
+
encoding: "utf8",
|
|
495
|
+
timeout: CLI_TIMEOUT_MS,
|
|
496
|
+
});
|
|
497
|
+
const output = cliOutput(result);
|
|
498
|
+
assert.notEqual(result.error && result.error.code, "ETIMEDOUT", output);
|
|
499
|
+
assert.equal(result.status, 1, output);
|
|
500
|
+
assert.match(output, /input must not be a symlink/);
|
|
501
|
+
assert.doesNotMatch(output, /stack|at validate-findings/i);
|
|
502
|
+
} finally {
|
|
503
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("CLI rejects input above the nesting-depth limit without an exception trace", () => {
|
|
508
|
+
const levels = LIMITS.nestingDepth + 1;
|
|
509
|
+
const result = runCli(`${"[".repeat(levels)}0${"]".repeat(levels)}`);
|
|
510
|
+
const output = cliOutput(result);
|
|
511
|
+
assert.equal(result.status, 1, output);
|
|
512
|
+
assert.match(output, new RegExp(`${LIMITS.nestingDepth} level nesting depth limit`));
|
|
513
|
+
assert.doesNotMatch(output, /RangeError|Maximum call stack|heap out of memory/i);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("CLI rejects an oversized array without an exception trace", () => {
|
|
517
|
+
const result = runCli(JSON.stringify(Array(LIMITS.arrayItems + 1).fill(null)));
|
|
518
|
+
const output = cliOutput(result);
|
|
519
|
+
assert.equal(result.status, 1, output);
|
|
520
|
+
assert.match(output, new RegExp(`${LIMITS.arrayItems} item array limit`));
|
|
521
|
+
assert.doesNotMatch(output, /RangeError|Maximum call stack|heap out of memory/i);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("checks pattern and branch invariants", () => {
|
|
525
|
+
rejectMutation(rejected, (finding) => { finding.fingerprint = "not stable"; });
|
|
526
|
+
rejectMutation(needsValidation, (finding) => {
|
|
527
|
+
finding.severity = { impact: { score: "low" }, overall_severity: "low" };
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("rejects unsupported and malformed schema keywords", () => {
|
|
532
|
+
assert(collectSchemaErrors({ type: "string", format: "uuid" }).some((error) => error.includes("format")));
|
|
533
|
+
assert(collectSchemaErrors({ type: "string", pattern: "[" }).some((error) => error.includes("regular expression")));
|
|
534
|
+
assert(collectSchemaErrors({ type: "string", visibleContent: "yes" }).some((error) => error.includes("expected boolean")));
|
|
535
|
+
assert(collectSchemaErrors({ type: "array", visibleContent: true }).some((error) => error.includes("requires type")));
|
|
536
|
+
assert.notEqual(validateDocument([], { type: "array", maxItems: 1 }).length, 0);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("caps malformed 1000-finding validation output", () => {
|
|
540
|
+
assert.equal(errorsFor(Array.from({ length: LIMITS.arrayItems }, () => null)).length, LIMITS.validationErrors);
|
|
541
|
+
if (!HAS_SAFE_INPUT_OPEN) return;
|
|
542
|
+
|
|
543
|
+
const result = runCli(JSON.stringify(Array.from({ length: LIMITS.arrayItems }, () => null)));
|
|
544
|
+
const output = cliOutput(result);
|
|
545
|
+
assert.notEqual(result.error && result.error.code, "ETIMEDOUT", output);
|
|
546
|
+
assert.equal(result.status, 1, output);
|
|
547
|
+
assert.match(output, /output capped at 100/);
|
|
548
|
+
assert(output.length < 20000, `unexpected output length ${output.length}`);
|
|
549
|
+
assert.doesNotMatch(output, /RangeError|Maximum call stack|stack|at validate-findings/i);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("caps amplified in-limit findings output under a constrained Node heap", { skip: !HAS_SAFE_INPUT_OPEN }, () => {
|
|
553
|
+
const findings = Array.from({ length: 750 }, () => ({
|
|
554
|
+
verdict: "confirmed",
|
|
555
|
+
trace: Array.from({ length: LIMITS.arrayItems }, () => 0),
|
|
556
|
+
evidence: Array.from({ length: LIMITS.arrayItems }, () => 0),
|
|
557
|
+
}));
|
|
558
|
+
const contents = JSON.stringify(findings);
|
|
559
|
+
assert(contents.length > 3 * 1000 * 1000, `hostile input too small: ${contents.length}`);
|
|
560
|
+
assert(contents.length <= LIMITS.inputBytes, `hostile input over limit: ${contents.length}`);
|
|
561
|
+
|
|
562
|
+
const result = runCli(contents, {
|
|
563
|
+
nodeArgs: ["--max-old-space-size=64"],
|
|
564
|
+
timeout: HOSTILE_CLI_TIMEOUT_MS,
|
|
565
|
+
});
|
|
566
|
+
const output = cliOutput(result);
|
|
567
|
+
assert.notEqual(result.error && result.error.code, "ETIMEDOUT", output);
|
|
568
|
+
assert.equal(result.status, 1, output);
|
|
569
|
+
assert.match(output, /output capped at 100/);
|
|
570
|
+
assert(output.length < 20000, `unexpected output length ${output.length}`);
|
|
571
|
+
assert.doesNotMatch(output, /heap out of memory|allocation failed|RangeError|Maximum call stack/i);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("keeps shared helpers aligned with the coverage-ledger validator", () => {
|
|
575
|
+
const findingsModule = require("./validate-findings.cjs");
|
|
576
|
+
const ledgerModule = require("./validate-coverage-ledger.cjs");
|
|
577
|
+
|
|
578
|
+
for (const name of [
|
|
579
|
+
"VISIBLE_CONTENT",
|
|
580
|
+
"PATH_FORBIDDEN_CHARACTER",
|
|
581
|
+
"WINDOWS_RESERVED_COMPONENT",
|
|
582
|
+
"UNSAFE_DIAGNOSTIC_CHARACTER",
|
|
583
|
+
]) {
|
|
584
|
+
assert.equal(findingsModule[name].source, ledgerModule[name].source, `${name} source`);
|
|
585
|
+
assert.equal(findingsModule[name].flags, ledgerModule[name].flags, `${name} flags`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const sharedLimitKeys = Object.keys(findingsModule.LIMITS)
|
|
589
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(ledgerModule.LIMITS, key))
|
|
590
|
+
.sort();
|
|
591
|
+
assert.deepEqual(sharedLimitKeys, ["inputBytes", "nestingDepth", "validationErrors"]);
|
|
592
|
+
for (const key of sharedLimitKeys) {
|
|
593
|
+
assert.equal(findingsModule.LIMITS[key], ledgerModule.LIMITS[key], `LIMITS.${key}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const pathCorpus = [
|
|
597
|
+
"src/handler.js",
|
|
598
|
+
"src/caf\u00e9/handler.js",
|
|
599
|
+
"src/\u65e5\u672c\u8a9e/\u0444\u0430\u0439\u043b.ts",
|
|
600
|
+
"src/cloc\u212a$.txt",
|
|
601
|
+
"src/CLOCK$.txt",
|
|
602
|
+
"src/con.txt",
|
|
603
|
+
"CON",
|
|
604
|
+
"src/COM\u00b9.log",
|
|
605
|
+
"src/lpt\u00b3",
|
|
606
|
+
"/etc/passwd",
|
|
607
|
+
"../src/file.c",
|
|
608
|
+
"src/../file.c",
|
|
609
|
+
"src//file.c",
|
|
610
|
+
"src\\file.c",
|
|
611
|
+
"src/file:name.c",
|
|
612
|
+
"~home/file.c",
|
|
613
|
+
"C:/file.c",
|
|
614
|
+
"src/file.c ",
|
|
615
|
+
"src/file.c.",
|
|
616
|
+
"src/file\u202ename.c",
|
|
617
|
+
"src/file\u200b.js",
|
|
618
|
+
"src/file\u034f.js",
|
|
619
|
+
"src/file\ufe0f.js",
|
|
620
|
+
"src/file\ud800name.c",
|
|
621
|
+
"src/file\udc00name.c",
|
|
622
|
+
];
|
|
623
|
+
for (const value of pathCorpus) {
|
|
624
|
+
assert.equal(
|
|
625
|
+
findingsModule.isSafeRelativeSourcePath(value),
|
|
626
|
+
ledgerModule.isSafeRelativePath(value),
|
|
627
|
+
`path verdict diverges for ${JSON.stringify(value)}`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
assert.equal(findingsModule.isSafeRelativeSourcePath("src/cloc\u212a$.txt"), false);
|
|
631
|
+
assert.equal(ledgerModule.isSafeRelativePath("src/cloc\u212a$.txt"), false);
|
|
632
|
+
|
|
633
|
+
const proseCorpus = [
|
|
634
|
+
"Valid prose.",
|
|
635
|
+
"caf\u00e9",
|
|
636
|
+
"",
|
|
637
|
+
" \t\r\n",
|
|
638
|
+
"\u200b",
|
|
639
|
+
"\u034f",
|
|
640
|
+
"\ufe0f",
|
|
641
|
+
"\ud800",
|
|
642
|
+
"\udc00",
|
|
643
|
+
"visible\ud800",
|
|
644
|
+
];
|
|
645
|
+
for (const value of proseCorpus) {
|
|
646
|
+
assert.equal(
|
|
647
|
+
findingsModule.hasVisibleProse(value),
|
|
648
|
+
ledgerModule.hasVisibleProse(value),
|
|
649
|
+
`prose verdict diverges for ${JSON.stringify(value)}`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
});
|