@rigour-labs/core 6.8.0 → 6.8.1
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/dist/inference/cloud-provider.js +12 -2
- package/package.json +11 -8
- package/dist/brief/briefing.test.d.ts +0 -1
- package/dist/brief/briefing.test.js +0 -107
- package/dist/context/automatic-index-cache.test.d.ts +0 -1
- package/dist/context/automatic-index-cache.test.js +0 -30
- package/dist/context/automatic-index.test.d.ts +0 -1
- package/dist/context/automatic-index.test.js +0 -45
- package/dist/context/cache-engine.test.d.ts +0 -1
- package/dist/context/cache-engine.test.js +0 -99
- package/dist/context/dependency-graph.test.d.ts +0 -1
- package/dist/context/dependency-graph.test.js +0 -13
- package/dist/context/index-status.test.d.ts +0 -1
- package/dist/context/index-status.test.js +0 -26
- package/dist/context.test.d.ts +0 -1
- package/dist/context.test.js +0 -228
- package/dist/deep/agent-review.test.d.ts +0 -1
- package/dist/deep/agent-review.test.js +0 -53
- package/dist/deep/code-context.test.d.ts +0 -1
- package/dist/deep/code-context.test.js +0 -48
- package/dist/deep/code-pass.test.d.ts +0 -1
- package/dist/deep/code-pass.test.js +0 -112
- package/dist/deep/code-review-prompt.test.d.ts +0 -1
- package/dist/deep/code-review-prompt.test.js +0 -20
- package/dist/deep/code-verifier.test.d.ts +0 -1
- package/dist/deep/code-verifier.test.js +0 -54
- package/dist/deep/diff-tests/calls.test.d.ts +0 -1
- package/dist/deep/diff-tests/calls.test.js +0 -33
- package/dist/deep/diff-tests/run.test.d.ts +0 -1
- package/dist/deep/diff-tests/run.test.js +0 -58
- package/dist/deep/fact-extractor.test.d.ts +0 -1
- package/dist/deep/fact-extractor.test.js +0 -581
- package/dist/deep/parse-findings.test.d.ts +0 -1
- package/dist/deep/parse-findings.test.js +0 -25
- package/dist/deep/pr-review.test.d.ts +0 -1
- package/dist/deep/pr-review.test.js +0 -80
- package/dist/deep/prompts.test.d.ts +0 -1
- package/dist/deep/prompts.test.js +0 -235
- package/dist/deep/reference-pack.test.d.ts +0 -1
- package/dist/deep/reference-pack.test.js +0 -46
- package/dist/deep/related-changes.test.d.ts +0 -1
- package/dist/deep/related-changes.test.js +0 -33
- package/dist/deep/review-context-export.test.d.ts +0 -1
- package/dist/deep/review-context-export.test.js +0 -32
- package/dist/deep/review-tools.test.d.ts +0 -1
- package/dist/deep/review-tools.test.js +0 -39
- package/dist/deep/risk.test.d.ts +0 -1
- package/dist/deep/risk.test.js +0 -101
- package/dist/deep/verifier.test.d.ts +0 -1
- package/dist/deep/verifier.test.js +0 -635
- package/dist/discovery.test.d.ts +0 -1
- package/dist/discovery.test.js +0 -93
- package/dist/environment.test.d.ts +0 -1
- package/dist/environment.test.js +0 -94
- package/dist/firewall/firewall.test.d.ts +0 -1
- package/dist/firewall/firewall.test.js +0 -117
- package/dist/firewall/trust-boundaries.test.d.ts +0 -1
- package/dist/firewall/trust-boundaries.test.js +0 -64
- package/dist/firewall/trusted-control.test.d.ts +0 -1
- package/dist/firewall/trusted-control.test.js +0 -188
- package/dist/gates/agent-team.test.d.ts +0 -1
- package/dist/gates/agent-team.test.js +0 -113
- package/dist/gates/ast.test.d.ts +0 -1
- package/dist/gates/ast.test.js +0 -112
- package/dist/gates/checkpoint.test.d.ts +0 -1
- package/dist/gates/checkpoint.test.js +0 -105
- package/dist/gates/content.test.d.ts +0 -1
- package/dist/gates/content.test.js +0 -73
- package/dist/gates/coverage.test.d.ts +0 -1
- package/dist/gates/coverage.test.js +0 -53
- package/dist/gates/dedupe-failures.test.d.ts +0 -1
- package/dist/gates/dedupe-failures.test.js +0 -12
- package/dist/gates/deep-analysis.test.d.ts +0 -1
- package/dist/gates/deep-analysis.test.js +0 -86
- package/dist/gates/deep-intent.test.d.ts +0 -1
- package/dist/gates/deep-intent.test.js +0 -51
- package/dist/gates/deep-timeout.test.d.ts +0 -1
- package/dist/gates/deep-timeout.test.js +0 -10
- package/dist/gates/deprecated-apis.test.d.ts +0 -1
- package/dist/gates/deprecated-apis.test.js +0 -318
- package/dist/gates/deprecated-dependencies.test.d.ts +0 -1
- package/dist/gates/deprecated-dependencies.test.js +0 -55
- package/dist/gates/frontend-secret-exposure.test.d.ts +0 -1
- package/dist/gates/frontend-secret-exposure.test.js +0 -148
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +0 -27
- package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/js-resolver-types.test.js +0 -14
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports-sveltekit.test.js +0 -132
- package/dist/gates/hallucinated-imports.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports.test.js +0 -1206
- package/dist/gates/js-style-context.test.d.ts +0 -1
- package/dist/gates/js-style-context.test.js +0 -35
- package/dist/gates/logic-drift.test.d.ts +0 -1
- package/dist/gates/logic-drift.test.js +0 -52
- package/dist/gates/phantom-apis.test.d.ts +0 -1
- package/dist/gates/phantom-apis.test.js +0 -396
- package/dist/gates/promise-safety.test.d.ts +0 -1
- package/dist/gates/promise-safety.test.js +0 -34
- package/dist/gates/runner.test.d.ts +0 -1
- package/dist/gates/runner.test.js +0 -77
- package/dist/gates/scoped-gates.test.d.ts +0 -1
- package/dist/gates/scoped-gates.test.js +0 -52
- package/dist/gates/security-patterns-owasp.test.d.ts +0 -1
- package/dist/gates/security-patterns-owasp.test.js +0 -186
- package/dist/gates/security-patterns.test.d.ts +0 -1
- package/dist/gates/security-patterns.test.js +0 -194
- package/dist/gates/semantic-bugs.test.d.ts +0 -1
- package/dist/gates/semantic-bugs.test.js +0 -76
- package/dist/gates/side-effect-analysis.test.d.ts +0 -1
- package/dist/gates/side-effect-analysis.test.js +0 -162
- package/dist/gates/style-drift.test.d.ts +0 -1
- package/dist/gates/style-drift.test.js +0 -26
- package/dist/gates/test-quality.test.d.ts +0 -1
- package/dist/gates/test-quality.test.js +0 -325
- package/dist/gates/trusted-reviews.test.d.ts +0 -1
- package/dist/gates/trusted-reviews.test.js +0 -16
- package/dist/gates/unindexed-reads/queries.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/queries.test.js +0 -54
- package/dist/gates/unindexed-reads/schema.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/schema.test.js +0 -81
- package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/unindexed-reads.test.js +0 -90
- package/dist/hooks/checker.test.d.ts +0 -1
- package/dist/hooks/checker.test.js +0 -159
- package/dist/hooks/dlp-confidence.test.d.ts +0 -1
- package/dist/hooks/dlp-confidence.test.js +0 -51
- package/dist/hooks/dlp-feedback.test.d.ts +0 -1
- package/dist/hooks/dlp-feedback.test.js +0 -131
- package/dist/hooks/input-validator.test.d.ts +0 -1
- package/dist/hooks/input-validator.test.js +0 -329
- package/dist/hooks/templates.test.d.ts +0 -1
- package/dist/hooks/templates.test.js +0 -27
- package/dist/inference/brain-placeholder.test.d.ts +0 -1
- package/dist/inference/brain-placeholder.test.js +0 -28
- package/dist/inference/cloud-provider.test.d.ts +0 -1
- package/dist/inference/cloud-provider.test.js +0 -139
- package/dist/inference/executable.test.d.ts +0 -1
- package/dist/inference/executable.test.js +0 -41
- package/dist/inference/http-download.test.d.ts +0 -1
- package/dist/inference/http-download.test.js +0 -109
- package/dist/inference/llama-engine-checksum.test.d.ts +0 -1
- package/dist/inference/llama-engine-checksum.test.js +0 -27
- package/dist/inference/llama-engine.test.d.ts +0 -1
- package/dist/inference/llama-engine.test.js +0 -51
- package/dist/inference/llama-process.test.d.ts +0 -1
- package/dist/inference/llama-process.test.js +0 -61
- package/dist/inference/local-model.test.d.ts +0 -1
- package/dist/inference/local-model.test.js +0 -23
- package/dist/inference/model-download.test.d.ts +0 -1
- package/dist/inference/model-download.test.js +0 -125
- package/dist/inference/model-manager.test.d.ts +0 -1
- package/dist/inference/model-manager.test.js +0 -24
- package/dist/inference/types.test.d.ts +0 -1
- package/dist/inference/types.test.js +0 -19
- package/dist/memory/recall.test.d.ts +0 -1
- package/dist/memory/recall.test.js +0 -36
- package/dist/pattern-index/indexer.test.d.ts +0 -6
- package/dist/pattern-index/indexer.test.js +0 -197
- package/dist/pattern-index/matcher.test.d.ts +0 -6
- package/dist/pattern-index/matcher.test.js +0 -238
- package/dist/pattern-index/pattern-reuse.test.d.ts +0 -1
- package/dist/pattern-index/pattern-reuse.test.js +0 -76
- package/dist/pattern-index/semantic-runtime.test.d.ts +0 -1
- package/dist/pattern-index/semantic-runtime.test.js +0 -31
- package/dist/pattern-index/staleness.test.d.ts +0 -6
- package/dist/pattern-index/staleness.test.js +0 -211
- package/dist/review/agent-fixes.test.d.ts +0 -1
- package/dist/review/agent-fixes.test.js +0 -32
- package/dist/review/backtest-init.test.d.ts +0 -1
- package/dist/review/backtest-init.test.js +0 -136
- package/dist/review/backtest-judges.test.d.ts +0 -1
- package/dist/review/backtest-judges.test.js +0 -32
- package/dist/review/backtest-last.test.d.ts +0 -1
- package/dist/review/backtest-last.test.js +0 -109
- package/dist/review/backtest.test.d.ts +0 -1
- package/dist/review/backtest.test.js +0 -183
- package/dist/review/baseline.test.d.ts +0 -1
- package/dist/review/baseline.test.js +0 -22
- package/dist/review/branch-checks.test.d.ts +0 -1
- package/dist/review/branch-checks.test.js +0 -48
- package/dist/review/check-outcomes.test.d.ts +0 -1
- package/dist/review/check-outcomes.test.js +0 -56
- package/dist/review/code-patterns.test.d.ts +0 -1
- package/dist/review/code-patterns.test.js +0 -142
- package/dist/review/dead-code.test.d.ts +0 -1
- package/dist/review/dead-code.test.js +0 -154
- package/dist/review/deep-runs.test.d.ts +0 -1
- package/dist/review/deep-runs.test.js +0 -23
- package/dist/review/effectiveness.test.d.ts +0 -1
- package/dist/review/effectiveness.test.js +0 -42
- package/dist/review/fix-scope.test.d.ts +0 -1
- package/dist/review/fix-scope.test.js +0 -104
- package/dist/review/generated-files.test.d.ts +0 -1
- package/dist/review/generated-files.test.js +0 -25
- package/dist/review/migration-order.test.d.ts +0 -1
- package/dist/review/migration-order.test.js +0 -62
- package/dist/review/quiet.test.d.ts +0 -1
- package/dist/review/quiet.test.js +0 -38
- package/dist/review/receipt.test.d.ts +0 -1
- package/dist/review/receipt.test.js +0 -60
- package/dist/review/review-task.test.d.ts +0 -1
- package/dist/review/review-task.test.js +0 -89
- package/dist/review/review.test.d.ts +0 -1
- package/dist/review/review.test.js +0 -203
- package/dist/review/reviewer/adapters.test.d.ts +0 -1
- package/dist/review/reviewer/adapters.test.js +0 -100
- package/dist/review/reviewer/api-judge.test.d.ts +0 -1
- package/dist/review/reviewer/api-judge.test.js +0 -108
- package/dist/review/reviewer/background.test.d.ts +0 -1
- package/dist/review/reviewer/background.test.js +0 -118
- package/dist/review/reviewer/context.test.d.ts +0 -1
- package/dist/review/reviewer/context.test.js +0 -45
- package/dist/review/reviewer/exec.test.d.ts +0 -1
- package/dist/review/reviewer/exec.test.js +0 -59
- package/dist/review/reviewer/inputs.test.d.ts +0 -1
- package/dist/review/reviewer/inputs.test.js +0 -52
- package/dist/review/reviewer/panel.test.d.ts +0 -1
- package/dist/review/reviewer/panel.test.js +0 -110
- package/dist/review/reviewer/record.test.d.ts +0 -1
- package/dist/review/reviewer/record.test.js +0 -40
- package/dist/review/reviewer/rule-writer.test.d.ts +0 -1
- package/dist/review/reviewer/rule-writer.test.js +0 -52
- package/dist/review/reviewer/settings.test.d.ts +0 -1
- package/dist/review/reviewer/settings.test.js +0 -57
- package/dist/review/reviewer/usage.test.d.ts +0 -1
- package/dist/review/reviewer/usage.test.js +0 -14
- package/dist/review/reviewer.test.d.ts +0 -1
- package/dist/review/reviewer.test.js +0 -792
- package/dist/review/stories.test.d.ts +0 -1
- package/dist/review/stories.test.js +0 -58
- package/dist/review/toolchain.test.d.ts +0 -1
- package/dist/review/toolchain.test.js +0 -108
- package/dist/review/typed/redundancy.test.d.ts +0 -1
- package/dist/review/typed/redundancy.test.js +0 -315
- package/dist/review/typed/schema-nullability.test.d.ts +0 -1
- package/dist/review/typed/schema-nullability.test.js +0 -63
- package/dist/review-learning/human-edits.test.d.ts +0 -1
- package/dist/review-learning/human-edits.test.js +0 -47
- package/dist/review-learning/repo-rules.test.d.ts +0 -1
- package/dist/review-learning/repo-rules.test.js +0 -93
- package/dist/review-learning/review-learning.test.d.ts +0 -1
- package/dist/review-learning/review-learning.test.js +0 -249
- package/dist/safety.test.d.ts +0 -1
- package/dist/safety.test.js +0 -42
- package/dist/semantic/benchmark.test.d.ts +0 -1
- package/dist/semantic/benchmark.test.js +0 -22
- package/dist/semantic/intent/intent.test.d.ts +0 -1
- package/dist/semantic/intent/intent.test.js +0 -46
- package/dist/semantic/learn/learn.test.d.ts +0 -1
- package/dist/semantic/learn/learn.test.js +0 -67
- package/dist/semantic/origins.test.d.ts +0 -1
- package/dist/semantic/origins.test.js +0 -77
- package/dist/semantic/project-facts.test.d.ts +0 -1
- package/dist/semantic/project-facts.test.js +0 -45
- package/dist/semantic/sites/call-sites.test.d.ts +0 -1
- package/dist/semantic/sites/call-sites.test.js +0 -46
- package/dist/services/adaptive-thresholds.test.d.ts +0 -1
- package/dist/services/adaptive-thresholds.test.js +0 -53
- package/dist/services/agent-history.test.d.ts +0 -1
- package/dist/services/agent-history.test.js +0 -69
- package/dist/services/context-scope-summary.test.d.ts +0 -1
- package/dist/services/context-scope-summary.test.js +0 -17
- package/dist/services/context-telemetry-service.test.d.ts +0 -1
- package/dist/services/context-telemetry-service.test.js +0 -182
- package/dist/services/cursor-usage-sync.test.d.ts +0 -1
- package/dist/services/cursor-usage-sync.test.js +0 -173
- package/dist/services/engineering-knowledge-graph.test.d.ts +0 -1
- package/dist/services/engineering-knowledge-graph.test.js +0 -76
- package/dist/services/model-pricing.test.d.ts +0 -1
- package/dist/services/model-pricing.test.js +0 -44
- package/dist/services/observed-savings.test.d.ts +0 -1
- package/dist/services/observed-savings.test.js +0 -37
- package/dist/services/score-history.test.d.ts +0 -1
- package/dist/services/score-history.test.js +0 -61
- package/dist/smoke.test.d.ts +0 -1
- package/dist/smoke.test.js +0 -17
- package/dist/storage/cache-cleanup.test.d.ts +0 -1
- package/dist/storage/cache-cleanup.test.js +0 -54
- package/dist/storage/context-telemetry.test.d.ts +0 -1
- package/dist/storage/context-telemetry.test.js +0 -80
- package/dist/storage/db.test.d.ts +0 -1
- package/dist/storage/db.test.js +0 -46
- package/dist/storage/fix-lessons.test.d.ts +0 -1
- package/dist/storage/fix-lessons.test.js +0 -61
- package/dist/storage/lessons.test.d.ts +0 -1
- package/dist/storage/lessons.test.js +0 -81
- package/dist/storage/local-encryption.test.d.ts +0 -1
- package/dist/storage/local-encryption.test.js +0 -34
- package/dist/storage/local-memory.test.d.ts +0 -1
- package/dist/storage/local-memory.test.js +0 -55
- package/dist/storage/share-memory.test.d.ts +0 -1
- package/dist/storage/share-memory.test.js +0 -34
- package/dist/storage/team-diagnostics.test.d.ts +0 -1
- package/dist/storage/team-diagnostics.test.js +0 -51
- package/dist/storage/team-scope.test.d.ts +0 -1
- package/dist/storage/team-scope.test.js +0 -22
- package/dist/storage/team-store.test.d.ts +0 -1
- package/dist/storage/team-store.test.js +0 -11
- package/dist/storage/team-sync-scope.test.d.ts +0 -1
- package/dist/storage/team-sync-scope.test.js +0 -100
- package/dist/storage/team-vector-store.test.d.ts +0 -1
- package/dist/storage/team-vector-store.test.js +0 -56
- package/dist/storage/telemetry-scope.test.d.ts +0 -1
- package/dist/storage/telemetry-scope.test.js +0 -38
- package/dist/task/thread.test.d.ts +0 -1
- package/dist/task/thread.test.js +0 -133
- package/dist/telemetry/telemetry.test.d.ts +0 -1
- package/dist/telemetry/telemetry.test.js +0 -64
- package/dist/types/index.test.d.ts +0 -1
- package/dist/types/index.test.js +0 -33
- package/dist/utils/command-line.test.d.ts +0 -1
- package/dist/utils/command-line.test.js +0 -8
- package/dist/utils/diff-removed.test.d.ts +0 -1
- package/dist/utils/diff-removed.test.js +0 -29
- package/dist/utils/diff.test.d.ts +0 -1
- package/dist/utils/diff.test.js +0 -38
- package/dist/utils/glob-paths.test.d.ts +0 -1
- package/dist/utils/glob-paths.test.js +0 -40
- package/dist/utils/profile.test.d.ts +0 -1
- package/dist/utils/profile.test.js +0 -68
- package/dist/utils/scanner.test.d.ts +0 -1
- package/dist/utils/scanner.test.js +0 -48
- package/dist/utils/scope.test.d.ts +0 -1
- package/dist/utils/scope.test.js +0 -38
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for OWASP-aligned security patterns added in v3.0.0.
|
|
3
|
-
* Covers: ReDoS, overly permissive code, unsafe output, missing input validation.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { SecurityPatternsGate, checkSecurityPatterns } from './security-patterns.js';
|
|
7
|
-
import * as fs from 'fs';
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
import * as os from 'os';
|
|
10
|
-
describe('SecurityPatternsGate — OWASP extended patterns', () => {
|
|
11
|
-
let testDir;
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owasp-test-'));
|
|
14
|
-
});
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
describe('ReDoS detection (OWASP #7)', () => {
|
|
19
|
-
it('should detect dynamic regex from user input', async () => {
|
|
20
|
-
const filePath = path.join(testDir, 'search.ts');
|
|
21
|
-
fs.writeFileSync(filePath, `
|
|
22
|
-
const pattern = new RegExp(req.query.search);
|
|
23
|
-
const matches = text.match(pattern);
|
|
24
|
-
`);
|
|
25
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
26
|
-
expect(vulns.some(v => v.type === 'redos')).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
it('detects request data and input-named arguments, not constants that contain those words', async () => {
|
|
29
|
-
const tainted = path.join(testDir, 'tainted.ts');
|
|
30
|
-
fs.writeFileSync(tainted, `
|
|
31
|
-
const a = new RegExp(\`^\${req.body.term}$\`);
|
|
32
|
-
const b = new RegExp(input, 'i');
|
|
33
|
-
`);
|
|
34
|
-
expect((await checkSecurityPatterns(tainted)).filter(v => v.type === 'redos')).toHaveLength(2);
|
|
35
|
-
const constant = path.join(testDir, 'constant.ts');
|
|
36
|
-
fs.writeFileSync(constant, `
|
|
37
|
-
const user = String.raw\`auth\\.uid\\(\\)\`;
|
|
38
|
-
const left = new RegExp(String.raw\`^\${user} ?= ?(\${NAME})$\`, 'i');
|
|
39
|
-
const byQuery = new RegExp(queryPrefix + '$');
|
|
40
|
-
`);
|
|
41
|
-
expect((await checkSecurityPatterns(constant)).filter(v => v.type === 'redos')).toEqual([]);
|
|
42
|
-
});
|
|
43
|
-
it('should detect nested quantifiers', async () => {
|
|
44
|
-
const filePath = path.join(testDir, 'regex.ts');
|
|
45
|
-
fs.writeFileSync(filePath, `
|
|
46
|
-
const re = /(?:a+)+b/;
|
|
47
|
-
`);
|
|
48
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
49
|
-
expect(vulns.some(v => v.type === 'redos')).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
it('should allow safe regex patterns', async () => {
|
|
52
|
-
const filePath = path.join(testDir, 'safe-regex.ts');
|
|
53
|
-
fs.writeFileSync(filePath, `
|
|
54
|
-
const re = /^[a-z]+$/;
|
|
55
|
-
`);
|
|
56
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
57
|
-
expect(vulns.filter(v => v.type === 'redos')).toHaveLength(0);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
describe('Overly Permissive Code (OWASP #9)', () => {
|
|
61
|
-
it('should detect CORS wildcard origin', async () => {
|
|
62
|
-
const filePath = path.join(testDir, 'server.ts');
|
|
63
|
-
fs.writeFileSync(filePath, `
|
|
64
|
-
import cors from 'cors';
|
|
65
|
-
app.use(cors({ origin: '*' }));
|
|
66
|
-
`);
|
|
67
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
68
|
-
expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
it('should detect CORS origin true', async () => {
|
|
71
|
-
const filePath = path.join(testDir, 'server2.ts');
|
|
72
|
-
fs.writeFileSync(filePath, `
|
|
73
|
-
app.use(cors({ origin: true }));
|
|
74
|
-
`);
|
|
75
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
76
|
-
expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
it('should detect 0.0.0.0 binding', async () => {
|
|
79
|
-
const filePath = path.join(testDir, 'listen.ts');
|
|
80
|
-
fs.writeFileSync(filePath, `
|
|
81
|
-
app.listen(3000, '0.0.0.0');
|
|
82
|
-
`);
|
|
83
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
84
|
-
expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
it('should detect chmod 777', async () => {
|
|
87
|
-
const filePath = path.join(testDir, 'perms.ts');
|
|
88
|
-
fs.writeFileSync(filePath, `
|
|
89
|
-
fs.chmod('/tmp/data', 0o777);
|
|
90
|
-
`);
|
|
91
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
92
|
-
expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
it('should detect wildcard CORS header', async () => {
|
|
95
|
-
const filePath = path.join(testDir, 'headers.ts');
|
|
96
|
-
fs.writeFileSync(filePath, `
|
|
97
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
98
|
-
`);
|
|
99
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
100
|
-
expect(vulns.some(v => v.type === 'overly_permissive')).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
it('should allow specific CORS origin', async () => {
|
|
103
|
-
const filePath = path.join(testDir, 'safe-cors.ts');
|
|
104
|
-
fs.writeFileSync(filePath, `
|
|
105
|
-
app.use(cors({ origin: 'https://myapp.com' }));
|
|
106
|
-
`);
|
|
107
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
108
|
-
expect(vulns.filter(v => v.type === 'overly_permissive')).toHaveLength(0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
describe('Unsafe Output Handling (OWASP #6)', () => {
|
|
112
|
-
it('should detect response reflecting user input', async () => {
|
|
113
|
-
const filePath = path.join(testDir, 'handler.ts');
|
|
114
|
-
fs.writeFileSync(filePath, `
|
|
115
|
-
app.get('/echo', (req, res) => {
|
|
116
|
-
res.send(req.query.msg);
|
|
117
|
-
});
|
|
118
|
-
`);
|
|
119
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
120
|
-
expect(vulns.some(v => v.type === 'unsafe_output')).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
it('should detect eval with user input', async () => {
|
|
123
|
-
const filePath = path.join(testDir, 'eval.ts');
|
|
124
|
-
fs.writeFileSync(filePath, `
|
|
125
|
-
eval(req.body.code);
|
|
126
|
-
`);
|
|
127
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
128
|
-
expect(vulns.some(v => v.type === 'unsafe_output')).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
it('should allow safe response patterns', async () => {
|
|
131
|
-
const filePath = path.join(testDir, 'safe-res.ts');
|
|
132
|
-
fs.writeFileSync(filePath, `
|
|
133
|
-
res.json({ status: 'ok', data: processedData });
|
|
134
|
-
`);
|
|
135
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
136
|
-
expect(vulns.filter(v => v.type === 'unsafe_output')).toHaveLength(0);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
describe('Missing Input Validation (OWASP #8)', () => {
|
|
140
|
-
it('should detect JSON.parse on raw body', async () => {
|
|
141
|
-
const filePath = path.join(testDir, 'parse.ts');
|
|
142
|
-
fs.writeFileSync(filePath, `
|
|
143
|
-
const data = JSON.parse(req.body);
|
|
144
|
-
`);
|
|
145
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
146
|
-
expect(vulns.some(v => v.type === 'missing_input_validation')).toBe(true);
|
|
147
|
-
});
|
|
148
|
-
it('should detect "as any" type assertion', async () => {
|
|
149
|
-
const filePath = path.join(testDir, 'assert.ts');
|
|
150
|
-
fs.writeFileSync(filePath, `
|
|
151
|
-
const user = payload as any;
|
|
152
|
-
`);
|
|
153
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
154
|
-
expect(vulns.some(v => v.type === 'missing_input_validation')).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
it('should allow validated JSON parse', async () => {
|
|
157
|
-
const filePath = path.join(testDir, 'safe-parse.ts');
|
|
158
|
-
fs.writeFileSync(filePath, `
|
|
159
|
-
const data = JSON.parse(rawString);
|
|
160
|
-
const validated = schema.parse(data);
|
|
161
|
-
`);
|
|
162
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
163
|
-
expect(vulns.filter(v => v.type === 'missing_input_validation')).toHaveLength(0);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
describe('config toggles for new patterns', () => {
|
|
167
|
-
it('should disable redos when configured', async () => {
|
|
168
|
-
const gate = new SecurityPatternsGate({ enabled: true, redos: false });
|
|
169
|
-
const filePath = path.join(testDir, 'regex.ts');
|
|
170
|
-
fs.writeFileSync(filePath, `
|
|
171
|
-
const pattern = new RegExp(req.query.search);
|
|
172
|
-
`);
|
|
173
|
-
const failures = await gate.run({ cwd: testDir });
|
|
174
|
-
expect(failures.filter(f => f.title?.includes('ReDoS') || f.title?.includes('regex'))).toHaveLength(0);
|
|
175
|
-
});
|
|
176
|
-
it('should disable overly_permissive when configured', async () => {
|
|
177
|
-
const gate = new SecurityPatternsGate({ enabled: true, overly_permissive: false });
|
|
178
|
-
const filePath = path.join(testDir, 'cors.ts');
|
|
179
|
-
fs.writeFileSync(filePath, `
|
|
180
|
-
app.use(cors({ origin: '*' }));
|
|
181
|
-
`);
|
|
182
|
-
const failures = await gate.run({ cwd: testDir });
|
|
183
|
-
expect(failures.filter(f => f.title?.includes('CORS') || f.title?.includes('permissive'))).toHaveLength(0);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { SecurityPatternsGate, checkSecurityPatterns } from './security-patterns.js';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
describe('SecurityPatternsGate', () => {
|
|
7
|
-
let testDir;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'security-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
describe('gate initialization', () => {
|
|
15
|
-
it('should create gate with default config', () => {
|
|
16
|
-
const gate = new SecurityPatternsGate();
|
|
17
|
-
expect(gate.id).toBe('security-patterns');
|
|
18
|
-
expect(gate.title).toBe('Security Pattern Detection');
|
|
19
|
-
});
|
|
20
|
-
it('should skip when not enabled', async () => {
|
|
21
|
-
const gate = new SecurityPatternsGate({ enabled: false });
|
|
22
|
-
const failures = await gate.run({ cwd: testDir });
|
|
23
|
-
expect(failures).toEqual([]);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
describe('SQL injection detection', () => {
|
|
27
|
-
it('should detect string concatenation in queries', async () => {
|
|
28
|
-
const filePath = path.join(testDir, 'db.ts');
|
|
29
|
-
fs.writeFileSync(filePath, `
|
|
30
|
-
const userId = req.params.id;
|
|
31
|
-
db.query("SELECT * FROM users WHERE id = " + userId + " LIMIT 1");
|
|
32
|
-
`);
|
|
33
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
34
|
-
expect(vulns.some(v => v.type === 'sql_injection')).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
it('should detect template literal SQL', async () => {
|
|
37
|
-
const filePath = path.join(testDir, 'query.ts');
|
|
38
|
-
fs.writeFileSync(filePath, `
|
|
39
|
-
db.execute(\`SELECT * FROM users WHERE id = \${userId}\`);
|
|
40
|
-
`);
|
|
41
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
42
|
-
expect(vulns.some(v => v.type === 'sql_injection')).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
it('should NOT flag non-SQL template interpolation in normal function calls', async () => {
|
|
45
|
-
const filePath = path.join(testDir, 'status.ts');
|
|
46
|
-
fs.writeFileSync(filePath, `
|
|
47
|
-
logger.info(\`Current version: \${current} -> \${latest}\`);
|
|
48
|
-
`);
|
|
49
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
50
|
-
expect(vulns.some(v => v.type === 'sql_injection')).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
describe('XSS detection', () => {
|
|
54
|
-
it('should detect innerHTML assignment', async () => {
|
|
55
|
-
const filePath = path.join(testDir, 'dom.js');
|
|
56
|
-
fs.writeFileSync(filePath, `
|
|
57
|
-
element.innerHTML = userInput;
|
|
58
|
-
`);
|
|
59
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
60
|
-
expect(vulns.some(v => v.type === 'xss')).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
it('should detect dangerouslySetInnerHTML', async () => {
|
|
63
|
-
const filePath = path.join(testDir, 'component.tsx');
|
|
64
|
-
fs.writeFileSync(filePath, `
|
|
65
|
-
<div dangerouslySetInnerHTML={{ __html: content }} />
|
|
66
|
-
`);
|
|
67
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
68
|
-
expect(vulns.some(v => v.type === 'xss')).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
describe('hardcoded secrets detection', () => {
|
|
72
|
-
it('should detect hardcoded API keys', async () => {
|
|
73
|
-
const filePath = path.join(testDir, 'config.ts');
|
|
74
|
-
fs.writeFileSync(filePath, `
|
|
75
|
-
const API_KEY = "sk-1234567890abcdefghijklmnopqrst";
|
|
76
|
-
`);
|
|
77
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
78
|
-
expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
it('should detect private keys', async () => {
|
|
81
|
-
const filePath = path.join(testDir, 'key.ts');
|
|
82
|
-
fs.writeFileSync(filePath, `
|
|
83
|
-
const privateKey = "-----BEGIN RSA PRIVATE KEY-----";
|
|
84
|
-
`);
|
|
85
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
86
|
-
expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
it('does not take a UI label that mentions a password for a secret', async () => {
|
|
89
|
-
const filePath = path.join(testDir, 'be.ts');
|
|
90
|
-
fs.writeFileSync(filePath, `
|
|
91
|
-
export default { authForm: { hidePassword: 'Схаваць пароль', showPassword: 'Show the password' } };
|
|
92
|
-
`);
|
|
93
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
94
|
-
expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
it('should detect password assignments', async () => {
|
|
97
|
-
const filePath = path.join(testDir, 'auth.ts');
|
|
98
|
-
fs.writeFileSync(filePath, `
|
|
99
|
-
const password = "supersecretpassword123";
|
|
100
|
-
`);
|
|
101
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
102
|
-
expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
describe('insecure randomness detection', () => {
|
|
106
|
-
it('should detect Math.random usage', async () => {
|
|
107
|
-
const filePath = path.join(testDir, 'token.ts');
|
|
108
|
-
fs.writeFileSync(filePath, `
|
|
109
|
-
const token = Math.random().toString(36);
|
|
110
|
-
`);
|
|
111
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
112
|
-
expect(vulns.some(v => v.type === 'insecure_randomness')).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
describe('command injection detection', () => {
|
|
116
|
-
it('detects imported shell execution with user input', async () => {
|
|
117
|
-
const filePath = path.join(testDir, 'shell.ts');
|
|
118
|
-
fs.writeFileSync(filePath, `
|
|
119
|
-
import { exec as runShell } from 'node:child_process';
|
|
120
|
-
runShell(\`ls \${req.query.path}\`);
|
|
121
|
-
`);
|
|
122
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
123
|
-
expect(vulns.some(v => v.type === 'command_injection')).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
it('does not treat RegExp.exec or unrelated methods as shell execution', async () => {
|
|
126
|
-
const filePath = path.join(testDir, 'template.ts');
|
|
127
|
-
fs.writeFileSync(filePath, `
|
|
128
|
-
const separator = HB_ELSE.exec(body);
|
|
129
|
-
const other = LIQUID_BRANCH.exec(user);
|
|
130
|
-
const local = { exec(value: string) { return value; } };
|
|
131
|
-
local.exec(request.body);
|
|
132
|
-
`);
|
|
133
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
134
|
-
expect(vulns.filter(v => v.type === 'command_injection')).toEqual([]);
|
|
135
|
-
});
|
|
136
|
-
it('detects namespace, require, and shell-enabled spawn calls', async () => {
|
|
137
|
-
const filePath = path.join(testDir, 'shell.js');
|
|
138
|
-
fs.writeFileSync(filePath, `
|
|
139
|
-
const cp = require('child_process');
|
|
140
|
-
const { exec: runShell } = require('node:child_process');
|
|
141
|
-
cp.exec(req.body.command);
|
|
142
|
-
runShell(user.command);
|
|
143
|
-
cp.spawn(process.env.SHELL, [input], { shell: true });
|
|
144
|
-
cp.spawn(process.env.SHELL, [input]);
|
|
145
|
-
`);
|
|
146
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
147
|
-
expect(vulns.filter(v => v.type === 'command_injection')).toHaveLength(3);
|
|
148
|
-
});
|
|
149
|
-
it('does not use an imported shell binding shadowed by a parameter', async () => {
|
|
150
|
-
const filePath = path.join(testDir, 'shadow.ts');
|
|
151
|
-
fs.writeFileSync(filePath, `
|
|
152
|
-
import { exec } from 'child_process';
|
|
153
|
-
function parse(exec: RegExp) { return exec.exec(body); }
|
|
154
|
-
function run(exec: (value: string) => void) { exec(user); }
|
|
155
|
-
`);
|
|
156
|
-
const vulns = await checkSecurityPatterns(filePath);
|
|
157
|
-
expect(vulns.filter(v => v.type === 'command_injection')).toEqual([]);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
describe('severity filtering', () => {
|
|
161
|
-
it('should block on high severity by default', async () => {
|
|
162
|
-
const gate = new SecurityPatternsGate({ enabled: true });
|
|
163
|
-
const filePath = path.join(testDir, 'test.ts');
|
|
164
|
-
fs.writeFileSync(filePath, `
|
|
165
|
-
element.innerHTML = userInput;
|
|
166
|
-
`);
|
|
167
|
-
const failures = await gate.run({ cwd: testDir });
|
|
168
|
-
expect(failures.length).toBeGreaterThan(0);
|
|
169
|
-
});
|
|
170
|
-
it('should respect block_on_severity threshold', async () => {
|
|
171
|
-
const gate = new SecurityPatternsGate({
|
|
172
|
-
enabled: true,
|
|
173
|
-
block_on_severity: 'critical'
|
|
174
|
-
});
|
|
175
|
-
// innerHTML is 'high', not 'critical'
|
|
176
|
-
const filePath = path.join(testDir, 'test.ts');
|
|
177
|
-
fs.writeFileSync(filePath, `
|
|
178
|
-
element.innerHTML = userInput;
|
|
179
|
-
`);
|
|
180
|
-
const failures = await gate.run({ cwd: testDir });
|
|
181
|
-
expect(failures).toHaveLength(0); // High severity not blocked
|
|
182
|
-
});
|
|
183
|
-
it('should skip fixture-style test files for gate-level scans', async () => {
|
|
184
|
-
const gate = new SecurityPatternsGate({ enabled: true });
|
|
185
|
-
const fixturePath = path.join(testDir, 'auth.test.ts');
|
|
186
|
-
fs.writeFileSync(fixturePath, `
|
|
187
|
-
const password = "supersecretpassword123";
|
|
188
|
-
eval(req.body.code);
|
|
189
|
-
`);
|
|
190
|
-
const failures = await gate.run({ cwd: testDir });
|
|
191
|
-
expect(failures).toHaveLength(0);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { SemanticBugsGate } from './semantic-bugs.js';
|
|
6
|
-
import { GateRunner } from './runner.js';
|
|
7
|
-
import { ConfigSchema } from '../types/index.js';
|
|
8
|
-
const LEAKY = [
|
|
9
|
-
'export async function notify(endpoint: string, signature: string) {',
|
|
10
|
-
" return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
|
|
11
|
-
'}',
|
|
12
|
-
].join('\n');
|
|
13
|
-
describe('SemanticBugsGate', () => {
|
|
14
|
-
let cwd;
|
|
15
|
-
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'semantic-gate-')); });
|
|
16
|
-
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
17
|
-
function write(rel, body) {
|
|
18
|
-
fs.mkdirSync(path.dirname(path.join(cwd, rel)), { recursive: true });
|
|
19
|
-
fs.writeFileSync(path.join(cwd, rel), body);
|
|
20
|
-
}
|
|
21
|
-
it('runs an unvalidated candidate rule only when it is named', async () => {
|
|
22
|
-
write('src/scroll.ts', "export function setup(fn: () => void) {\n window.addEventListener('a', fn);\n addEventListener('b', fn);\n}\n");
|
|
23
|
-
expect(await new SemanticBugsGate().run({ cwd })).toEqual([]);
|
|
24
|
-
const named = await new SemanticBugsGate({ rules: ['env/bare-browser-global'] }).run({ cwd });
|
|
25
|
-
expect(named.map(f => [f.category, f.line])).toEqual([['env/bare-browser-global', 3]]);
|
|
26
|
-
});
|
|
27
|
-
it('reports a proven finding with its evidence, rule and verified metadata', async () => {
|
|
28
|
-
write('src/notify.ts', LEAKY);
|
|
29
|
-
const [failure, ...rest] = await new SemanticBugsGate().run({ cwd });
|
|
30
|
-
expect(rest).toHaveLength(0);
|
|
31
|
-
expect(failure).toMatchObject({
|
|
32
|
-
id: 'semantic-bugs', files: ['src/notify.ts'], line: 2, severity: 'high',
|
|
33
|
-
provenance: 'security', source: 'ast', category: 'credential-redirect', verified: true, confidence: 1,
|
|
34
|
-
});
|
|
35
|
-
expect(failure.title).toMatch(/^\[credential-redirect\]/);
|
|
36
|
-
expect(failure.details).toContain('at src/notify.ts:2');
|
|
37
|
-
});
|
|
38
|
-
it('skips test files and declaration files', async () => {
|
|
39
|
-
write('src/notify.test.ts', LEAKY);
|
|
40
|
-
write('src/__tests__/notify.ts', LEAKY);
|
|
41
|
-
write('types/notify.d.ts', 'export declare function notify(): void;\n');
|
|
42
|
-
expect(await new SemanticBugsGate().run({ cwd })).toEqual([]);
|
|
43
|
-
});
|
|
44
|
-
it('honours a scoped run', async () => {
|
|
45
|
-
write('src/notify.ts', LEAKY);
|
|
46
|
-
write('src/other.ts', LEAKY);
|
|
47
|
-
const failures = await new SemanticBugsGate().run({ cwd, patterns: ['src/other.ts'] });
|
|
48
|
-
expect(failures.map(f => f.files?.[0])).toEqual(['src/other.ts']);
|
|
49
|
-
});
|
|
50
|
-
it('runs only the configured rules', async () => {
|
|
51
|
-
write('src/notify.ts', LEAKY);
|
|
52
|
-
expect(await new SemanticBugsGate({ rules: ['in-memory-aggregation'] }).run({ cwd })).toEqual([]);
|
|
53
|
-
});
|
|
54
|
-
it('runs learned rules from .rigour/rules next to the built-in ones', async () => {
|
|
55
|
-
write('src/load.ts', "export const load = (url: string) => fetch(url, { method: 'GET' });\n");
|
|
56
|
-
write('.rigour/rules/require-option-redirect-abcd1234.json', JSON.stringify({
|
|
57
|
-
id: 'require-option-redirect-abcd1234', version: 1,
|
|
58
|
-
pattern: { template: 'require-option', callee: { level: 'name', key: 'fetch' }, argIndex: 1, property: 'redirect' },
|
|
59
|
-
message: 'fetch(...) does not set `redirect`.', severity: 'medium',
|
|
60
|
-
source: { commit: 'abcdef0123', file: 'src/http.ts', function: 'post' },
|
|
61
|
-
validation: { firesBefore: 1, firesAfter: 0, maxHits: 3, repoHits: [] },
|
|
62
|
-
}));
|
|
63
|
-
const [failure, ...rest] = await new SemanticBugsGate().run({ cwd });
|
|
64
|
-
expect(rest).toHaveLength(0);
|
|
65
|
-
expect(failure).toMatchObject({ files: ['src/load.ts'], line: 1, category: 'learned-rule', severity: 'medium' });
|
|
66
|
-
expect(failure.title).toContain('[learned/require-option-redirect-abcd1234]');
|
|
67
|
-
expect(failure.hint).toContain('fix abcdef01 in src/http.ts (post)');
|
|
68
|
-
});
|
|
69
|
-
it('is on by default and can be turned off in rigour.yml', async () => {
|
|
70
|
-
write('src/notify.ts', LEAKY);
|
|
71
|
-
const on = await new GateRunner(ConfigSchema.parse({ version: 1 })).run(cwd);
|
|
72
|
-
expect(on.failures.some(f => f.id === 'semantic-bugs')).toBe(true);
|
|
73
|
-
const off = await new GateRunner(ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: false } } })).run(cwd);
|
|
74
|
-
expect(off.failures.some(f => f.id === 'semantic-bugs')).toBe(false);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { SideEffectAnalysisGate } from './side-effect-analysis/index.js';
|
|
6
|
-
describe('SideEffectAnalysisGate', () => {
|
|
7
|
-
let cwd;
|
|
8
|
-
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'side-effect-')); });
|
|
9
|
-
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
10
|
-
async function titlesFor(file, source) {
|
|
11
|
-
fs.mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
|
|
12
|
-
fs.writeFileSync(path.join(cwd, file), source);
|
|
13
|
-
const failures = await new SideEffectAnalysisGate().run({ cwd });
|
|
14
|
-
return failures.map(f => f.title);
|
|
15
|
-
}
|
|
16
|
-
describe('recursion', () => {
|
|
17
|
-
it('ends an expression-bodied arrow at its expression, not at the next block', async () => {
|
|
18
|
-
// Shape of scripts/measure-intent.mjs: a one-line helper used inside a later loop that logs.
|
|
19
|
-
const titles = await titlesFor('scripts/measure.mjs', [
|
|
20
|
-
'const pct = (n, d) => (d === 0 ? \'n/a\' : `${Math.round((100 * n) / d)}%`);',
|
|
21
|
-
'for (const run of [1, 2]) {',
|
|
22
|
-
' process.stderr.write(`run ${run}: ${pct(run, 2)}\\n`);',
|
|
23
|
-
'}',
|
|
24
|
-
].join('\n'));
|
|
25
|
-
expect(titles).not.toContain('Side-Effect: Unbounded Recursion');
|
|
26
|
-
});
|
|
27
|
-
it('still flags a block-bodied arrow that recurses with I/O and no base case', async () => {
|
|
28
|
-
const titles = await titlesFor('src/walk.ts', [
|
|
29
|
-
"import fs from 'fs';",
|
|
30
|
-
'export const walk = (dir) => {',
|
|
31
|
-
' for (const entry of fs.readdirSync(dir)) walk(`${dir}/${entry}`);',
|
|
32
|
-
'};',
|
|
33
|
-
].join('\n'));
|
|
34
|
-
expect(titles).toContain('Side-Effect: Unbounded Recursion');
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
describe('process spawns', () => {
|
|
38
|
-
it('does not treat RegExp.prototype.exec as a process spawn', async () => {
|
|
39
|
-
// Verbatim shape of the SvelteKit repo's scripts/lint-fonts.mjs.
|
|
40
|
-
const titles = await titlesFor('scripts/lint-fonts.mjs', [
|
|
41
|
-
'function checkFontFamilyDeclarations(file, source) {',
|
|
42
|
-
'\tconst findings = [];',
|
|
43
|
-
'\tconst re = /font-family\\s*:/gi;',
|
|
44
|
-
'\tlet match;',
|
|
45
|
-
'\twhile ((match = re.exec(source)) !== null) {',
|
|
46
|
-
'\t\tconst valueStart = match.index + match[0].length;',
|
|
47
|
-
'\t\tfindings.push({ file, valueStart });',
|
|
48
|
-
'\t}',
|
|
49
|
-
'\treturn findings;',
|
|
50
|
-
'}',
|
|
51
|
-
].join('\n'));
|
|
52
|
-
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
it('does not treat a database exec method as a process spawn', async () => {
|
|
55
|
-
const titles = await titlesFor('src/db.ts', "export function migrate(db: any) { db.exec('create table t (id int)'); }\n");
|
|
56
|
-
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
it('does not flag a synchronous execFileSync call', async () => {
|
|
59
|
-
const titles = await titlesFor('scripts/eval/run.ts', [
|
|
60
|
-
"import { execFileSync } from 'node:child_process';",
|
|
61
|
-
"export const revision = () => execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();",
|
|
62
|
-
].join('\n'));
|
|
63
|
-
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
it('accepts once("exit") as exit handling for a spawned process', async () => {
|
|
66
|
-
const titles = await titlesFor('scripts/upload.ts', [
|
|
67
|
-
"import { spawn } from 'node:child_process';",
|
|
68
|
-
'export async function upload(args: string[]) {',
|
|
69
|
-
' const code = await new Promise<number>((resolve, reject) => {',
|
|
70
|
-
" const child = spawn('npx', args, { stdio: 'inherit' });",
|
|
71
|
-
" child.once('error', reject);",
|
|
72
|
-
" child.once('exit', (c) => resolve(c ?? 1));",
|
|
73
|
-
' });',
|
|
74
|
-
' return code;',
|
|
75
|
-
'}',
|
|
76
|
-
].join('\n'));
|
|
77
|
-
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
it('does not read prose in a comment as a fork call', async () => {
|
|
80
|
-
const titles = await titlesFor('src/lib/seo/json-ld.ts', [
|
|
81
|
-
"// Canonical profiles, ported as data from the fork (verified against production).",
|
|
82
|
-
'export const profiles = [];',
|
|
83
|
-
].join('\n'));
|
|
84
|
-
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
it('still flags a child_process exec whose process is never handled', async () => {
|
|
87
|
-
const titles = await titlesFor('src/run.ts', [
|
|
88
|
-
"import { exec } from 'child_process';",
|
|
89
|
-
'export function run() {',
|
|
90
|
-
" exec('ls -la');",
|
|
91
|
-
'}',
|
|
92
|
-
].join('\n'));
|
|
93
|
-
expect(titles.some(t => /Orphan Process/.test(t))).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
it('still flags a spawn through a child_process namespace', async () => {
|
|
96
|
-
const titles = await titlesFor('src/run.ts', [
|
|
97
|
-
"import * as cp from 'node:child_process';",
|
|
98
|
-
'export function run() {',
|
|
99
|
-
" cp.spawn('node', ['worker.js']);",
|
|
100
|
-
'}',
|
|
101
|
-
].join('\n'));
|
|
102
|
-
expect(titles.some(t => /Orphan Process/.test(t))).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
describe('timers', () => {
|
|
106
|
-
it('does not flag a one-shot awaited setTimeout delay', async () => {
|
|
107
|
-
const titles = await titlesFor('scripts/deploy.ts', [
|
|
108
|
-
'export async function retry(attempt: number) {',
|
|
109
|
-
' await new Promise((resolve) => setTimeout(resolve, attempt * 250));',
|
|
110
|
-
'}',
|
|
111
|
-
].join('\n'));
|
|
112
|
-
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
it('pairs a timer kept on a private field with a clearInterval in another method', async () => {
|
|
115
|
-
const titles = await titlesFor('src/lib/review-state.svelte.ts', [
|
|
116
|
-
'export class ReviewState {',
|
|
117
|
-
' #progressTimer: ReturnType<typeof setInterval> | undefined;',
|
|
118
|
-
' start() {',
|
|
119
|
-
' this.#progressTimer = setInterval(() => this.tick(), 500);',
|
|
120
|
-
' }',
|
|
121
|
-
' stop() {',
|
|
122
|
-
' clearInterval(this.#progressTimer);',
|
|
123
|
-
' }',
|
|
124
|
-
' tick() {}',
|
|
125
|
-
'}',
|
|
126
|
-
].join('\n'));
|
|
127
|
-
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
it('pairs a timer kept in an outer variable with a clearInterval in another function', async () => {
|
|
130
|
-
const titles = await titlesFor('src/lib/poller.ts', [
|
|
131
|
-
'let pollTimer: ReturnType<typeof setInterval> | undefined;',
|
|
132
|
-
'export function startPolling(refresh: () => void) {',
|
|
133
|
-
' pollTimer = setInterval(refresh, 5000);',
|
|
134
|
-
'}',
|
|
135
|
-
'export function stopPolling() {',
|
|
136
|
-
' clearInterval(pollTimer);',
|
|
137
|
-
'}',
|
|
138
|
-
].join('\n'));
|
|
139
|
-
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(false);
|
|
140
|
-
});
|
|
141
|
-
it('still flags a local interval whose own function never clears it', async () => {
|
|
142
|
-
const titles = await titlesFor('src/lib/leak.ts', [
|
|
143
|
-
'export function start(refresh: () => void) {',
|
|
144
|
-
' const timer = setInterval(refresh, 5000);',
|
|
145
|
-
' return 1;',
|
|
146
|
-
'}',
|
|
147
|
-
'export function stop(other: ReturnType<typeof setInterval>) {',
|
|
148
|
-
' clearInterval(other);',
|
|
149
|
-
'}',
|
|
150
|
-
].join('\n'));
|
|
151
|
-
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(true);
|
|
152
|
-
});
|
|
153
|
-
it('still flags a setInterval that is never cleared', async () => {
|
|
154
|
-
const titles = await titlesFor('src/poll.ts', [
|
|
155
|
-
'export function poll(check: () => void) {',
|
|
156
|
-
' setInterval(check, 1000);',
|
|
157
|
-
'}',
|
|
158
|
-
].join('\n'));
|
|
159
|
-
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(true);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import { StyleDriftGate } from './style-drift.js';
|
|
6
|
-
describe('StyleDriftGate', () => {
|
|
7
|
-
let cwd;
|
|
8
|
-
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'style-drift-')); });
|
|
9
|
-
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
10
|
-
it('does not compare default imports against unrelated named imports', async () => {
|
|
11
|
-
const src = path.join(cwd, 'src');
|
|
12
|
-
fs.mkdirSync(src);
|
|
13
|
-
for (let i = 0; i < 5; i++) {
|
|
14
|
-
fs.writeFileSync(path.join(src, `named${i}.ts`), `import { value } from './shared';\nexport function helper${i}() { return value; }\n`);
|
|
15
|
-
}
|
|
16
|
-
fs.writeFileSync(path.join(src, 'mui.ts'), [
|
|
17
|
-
"import Box from '@mui/material/Box';",
|
|
18
|
-
"import TextField from '@mui/material/TextField';",
|
|
19
|
-
"import Button from '@mui/material/Button';",
|
|
20
|
-
].join('\n'));
|
|
21
|
-
const gate = new StyleDriftGate();
|
|
22
|
-
expect(await gate.run({ cwd })).toEqual([]);
|
|
23
|
-
const findings = await gate.run({ cwd });
|
|
24
|
-
expect(findings.some(finding => finding.details.includes('import style'))).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|