Skip to content

Commit 6ed95f3

Browse files
Add vitest per-scanner fixture tests for v1-a.
Wire npm test to vitest and cover all eight scanners against test-data fixtures so regressions are caught before CI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d15be8e commit 6ed95f3

11 files changed

Lines changed: 1805 additions & 16 deletions

package-lock.json

Lines changed: 1593 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"build": "tsc",
1717
"start": "node dist/index.js",
1818
"dev": "ts-node src/index.ts",
19-
"test": "echo \"Error: no test specified\" && exit 1"
19+
"test": "vitest run"
2020
},
2121
"repository": {
2222
"type": "git",
@@ -40,7 +40,8 @@
4040
"devDependencies": {
4141
"@types/node": "^22.14.1",
4242
"ts-node": "^10.9.2",
43-
"typescript": "^5.8.3"
43+
"typescript": "^5.8.3",
44+
"vitest": "^3.2.4"
4445
},
4546
"dependencies": {
4647
"@types/js-yaml": "^4.0.9",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, it, expect } from 'vitest';
2+
import path from 'path';
3+
import { scanConfigFile } from '../../src/scanners/configuration';
4+
5+
describe('configuration scanner', () => {
6+
it('detects CORS High and DEBUG Medium in app.config.json', () => {
7+
const file = path.join(__dirname, '../../test-data/app.config.json');
8+
const findings = scanConfigFile(file);
9+
10+
const corsHigh = findings.filter(
11+
(f) => f.type === 'Permissive CORS' && f.severity === 'High',
12+
);
13+
const debugMedium = findings.filter(
14+
(f) => f.type === 'Insecure Setting' && f.severity === 'Medium',
15+
);
16+
17+
expect(corsHigh.length).toBeGreaterThanOrEqual(1);
18+
expect(debugMedium.length).toBeGreaterThanOrEqual(1);
19+
});
20+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, it, expect } from 'vitest';
2+
import path from 'path';
3+
import { parseDependencies, lookupCves } from '../../src/scanners/dependencies';
4+
5+
describe('dependencies scanner', () => {
6+
it('finds vulnerabilities for lodash or axios in vulnerable-deps/package.json', async () => {
7+
const manifestPath = path.join(__dirname, '../../test-data/vulnerable-deps/package.json');
8+
const dependencies = parseDependencies({
9+
npm: { manifest: manifestPath },
10+
});
11+
12+
expect(dependencies.some((d) => d.name === 'lodash' || d.name === 'axios')).toBe(true);
13+
14+
const findings = await lookupCves(dependencies);
15+
const vulnerable = findings.filter(
16+
(f) => (f.name === 'lodash' || f.name === 'axios') && f.vulnerabilities.length > 0,
17+
);
18+
19+
expect(vulnerable.length).toBeGreaterThan(0);
20+
});
21+
});

tests/scanners/endpoints.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect } from 'vitest';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { scanForExposedEndpoints } from '../../src/scanners/endpoints';
5+
6+
const detectedTech = {
7+
hasFrontend: false,
8+
hasBackend: true,
9+
isNextJs: false,
10+
hasAuth: false,
11+
hasMiddleware: false,
12+
hasHttpClient: false,
13+
hasCors: false,
14+
hasFileUpload: false,
15+
};
16+
17+
describe('endpoints scanner', () => {
18+
it('flags /admin with Medium or higher severity in endpoint-test.js', () => {
19+
const rootDir = path.join(__dirname, '../../test-data');
20+
const file = path.join(rootDir, 'endpoint-test.js');
21+
const content = fs.readFileSync(file, 'utf-8');
22+
23+
const findings = scanForExposedEndpoints(rootDir, file, content, detectedTech);
24+
const adminFindings = findings.filter(
25+
(f) => f.path.includes('/admin') && (f.severity === 'Medium' || f.severity === 'High' || f.severity === 'Critical'),
26+
);
27+
28+
expect(adminFindings.length).toBeGreaterThanOrEqual(1);
29+
});
30+
});

tests/scanners/httpClient.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it, expect } from 'vitest';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { scanForHttpClientIssues } from '../../src/scanners/httpClient';
5+
6+
describe('httpClient scanner', () => {
7+
it('detects at least one missing timeout in http-client-unsafe.js', () => {
8+
const file = path.join(__dirname, '../../test-data/http-client-unsafe.js');
9+
const content = fs.readFileSync(file, 'utf-8');
10+
11+
const findings = scanForHttpClientIssues(file, content, true);
12+
const missingTimeout = findings.filter((f) => f.type === 'Potential Missing Timeout');
13+
14+
expect(missingTimeout.length).toBeGreaterThanOrEqual(1);
15+
});
16+
});

tests/scanners/logging.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it, expect } from 'vitest';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { scanForLoggingIssues } from '../../src/scanners/logging';
5+
6+
describe('logging scanner', () => {
7+
it('detects at least one PII finding in logging-test.js', () => {
8+
const file = path.join(__dirname, '../../test-data/logging-test.js');
9+
const content = fs.readFileSync(file, 'utf-8');
10+
11+
const findings = scanForLoggingIssues(file, content, true);
12+
const piiFindings = findings.filter((f) => f.type === 'Potential PII Logging');
13+
14+
expect(piiFindings.length).toBeGreaterThanOrEqual(1);
15+
});
16+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect } from 'vitest';
2+
import path from 'path';
3+
import { checkRateLimitHeuristic } from '../../src/scanners/rateLimiting';
4+
import type { DependencyInfo } from '../../src/scanners/dependencies';
5+
6+
const detectedTech = {
7+
hasFrontend: false,
8+
hasBackend: true,
9+
isNextJs: false,
10+
hasAuth: false,
11+
hasMiddleware: false,
12+
hasHttpClient: false,
13+
hasCors: false,
14+
hasFileUpload: false,
15+
};
16+
17+
describe('rateLimiting scanner', () => {
18+
it('issues advisory when routes exist but no rate-limit package is in dependencies', () => {
19+
const missingFile = path.join(__dirname, '../../test-data/rate-limit-missing.js');
20+
const dependencies: DependencyInfo[] = [
21+
{
22+
name: 'express',
23+
version: '4.18.0',
24+
packageManager: 'npm',
25+
sourceFile: 'package.json',
26+
},
27+
];
28+
29+
const findings = checkRateLimitHeuristic(dependencies, [missingFile], detectedTech);
30+
31+
expect(findings.length).toBe(1);
32+
expect(findings[0].type).toBe('Project-Level Rate Limit Advisory');
33+
});
34+
35+
it('returns no advisory when express-rate-limit is in dependencies', () => {
36+
const presentFile = path.join(__dirname, '../../test-data/rate-limit-present.js');
37+
const dependencies: DependencyInfo[] = [
38+
{
39+
name: 'express',
40+
version: '4.18.0',
41+
packageManager: 'npm',
42+
sourceFile: 'package.json',
43+
},
44+
{
45+
name: 'express-rate-limit',
46+
version: '6.7.0',
47+
packageManager: 'npm',
48+
sourceFile: 'package.json',
49+
},
50+
];
51+
52+
const findings = checkRateLimitHeuristic(dependencies, [presentFile], detectedTech);
53+
54+
expect(findings).toHaveLength(0);
55+
});
56+
});

tests/scanners/secrets.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { scanFileForSecrets } from '../../src/scanners/secrets';
3+
import path from 'path';
4+
5+
describe('secrets scanner', () => {
6+
it('detects High severity AWS keys in aws-secrets-tests.txt', () => {
7+
const file = path.join(__dirname, '../../test-data/aws-secrets-tests.txt');
8+
const findings = scanFileForSecrets(file);
9+
const high = findings.filter((f) => f.severity === 'High');
10+
expect(high.length).toBeGreaterThanOrEqual(1);
11+
});
12+
13+
it('returns no High findings for safe-file.txt', () => {
14+
const file = path.join(__dirname, '../../test-data/safe-file.txt');
15+
const findings = scanFileForSecrets(file);
16+
const high = findings.filter((f) => f.severity === 'High');
17+
expect(high).toHaveLength(0);
18+
});
19+
});

tests/scanners/uploads.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it, expect } from 'vitest';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { scanForUnvalidatedUploads } from '../../src/scanners/uploads';
5+
6+
describe('uploads scanner', () => {
7+
it('reports missing multer limits when hasBackend is true', () => {
8+
const file = path.join(__dirname, '../../test-data/multer-test.js');
9+
const content = fs.readFileSync(file, 'utf-8');
10+
11+
const findings = scanForUnvalidatedUploads(file, content, true);
12+
const missingLimits = findings.filter((f) => f.type === 'Missing Upload Size Limit');
13+
14+
expect(missingLimits.length).toBeGreaterThanOrEqual(1);
15+
});
16+
});

0 commit comments

Comments
 (0)