agent-security-scanner-mcp 3.20.0 → 4.0.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/README.md +144 -43
- package/code-review-agent/.env.example +8 -0
- package/code-review-agent/README.md +142 -0
- package/code-review-agent/TODO.md +149 -0
- package/code-review-agent/bin/cr-agent.ts +313 -0
- package/code-review-agent/dist/bin/cr-agent.d.ts +3 -0
- package/code-review-agent/dist/bin/cr-agent.d.ts.map +1 -0
- package/code-review-agent/dist/bin/cr-agent.js +299 -0
- package/code-review-agent/dist/bin/cr-agent.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/engine.d.ts +16 -0
- package/code-review-agent/dist/src/analyzer/engine.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/engine.js +298 -0
- package/code-review-agent/dist/src/analyzer/engine.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/intent.d.ts +10 -0
- package/code-review-agent/dist/src/analyzer/intent.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/intent.js +40 -0
- package/code-review-agent/dist/src/analyzer/intent.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/semantic.d.ts +19 -0
- package/code-review-agent/dist/src/analyzer/semantic.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/semantic.js +150 -0
- package/code-review-agent/dist/src/analyzer/semantic.js.map +1 -0
- package/code-review-agent/dist/src/context/assembler.d.ts +16 -0
- package/code-review-agent/dist/src/context/assembler.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/assembler.js +135 -0
- package/code-review-agent/dist/src/context/assembler.js.map +1 -0
- package/code-review-agent/dist/src/context/file.d.ts +6 -0
- package/code-review-agent/dist/src/context/file.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/file.js +139 -0
- package/code-review-agent/dist/src/context/file.js.map +1 -0
- package/code-review-agent/dist/src/context/project.d.ts +4 -0
- package/code-review-agent/dist/src/context/project.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/project.js +252 -0
- package/code-review-agent/dist/src/context/project.js.map +1 -0
- package/code-review-agent/dist/src/graph/dependency.d.ts +11 -0
- package/code-review-agent/dist/src/graph/dependency.d.ts.map +1 -0
- package/code-review-agent/dist/src/graph/dependency.js +102 -0
- package/code-review-agent/dist/src/graph/dependency.js.map +1 -0
- package/code-review-agent/dist/src/graph/resolver.d.ts +9 -0
- package/code-review-agent/dist/src/graph/resolver.d.ts.map +1 -0
- package/code-review-agent/dist/src/graph/resolver.js +124 -0
- package/code-review-agent/dist/src/graph/resolver.js.map +1 -0
- package/code-review-agent/dist/src/index.d.ts +21 -0
- package/code-review-agent/dist/src/index.d.ts.map +1 -0
- package/code-review-agent/dist/src/index.js +21 -0
- package/code-review-agent/dist/src/index.js.map +1 -0
- package/code-review-agent/dist/src/llm/anthropic.d.ts +13 -0
- package/code-review-agent/dist/src/llm/anthropic.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/anthropic.js +83 -0
- package/code-review-agent/dist/src/llm/anthropic.js.map +1 -0
- package/code-review-agent/dist/src/llm/claude-cli.d.ts +13 -0
- package/code-review-agent/dist/src/llm/claude-cli.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/claude-cli.js +142 -0
- package/code-review-agent/dist/src/llm/claude-cli.js.map +1 -0
- package/code-review-agent/dist/src/llm/openai.d.ts +13 -0
- package/code-review-agent/dist/src/llm/openai.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/openai.js +78 -0
- package/code-review-agent/dist/src/llm/openai.js.map +1 -0
- package/code-review-agent/dist/src/llm/provider.d.ts +18 -0
- package/code-review-agent/dist/src/llm/provider.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/provider.js +11 -0
- package/code-review-agent/dist/src/llm/provider.js.map +1 -0
- package/code-review-agent/dist/src/llm/router.d.ts +14 -0
- package/code-review-agent/dist/src/llm/router.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/router.js +67 -0
- package/code-review-agent/dist/src/llm/router.js.map +1 -0
- package/code-review-agent/dist/src/llm/schemas.d.ts +18 -0
- package/code-review-agent/dist/src/llm/schemas.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/schemas.js +91 -0
- package/code-review-agent/dist/src/llm/schemas.js.map +1 -0
- package/code-review-agent/dist/src/types/analysis.d.ts +56 -0
- package/code-review-agent/dist/src/types/analysis.d.ts.map +1 -0
- package/code-review-agent/dist/src/types/analysis.js +2 -0
- package/code-review-agent/dist/src/types/analysis.js.map +1 -0
- package/code-review-agent/dist/src/types/config.d.ts +24 -0
- package/code-review-agent/dist/src/types/config.d.ts.map +1 -0
- package/code-review-agent/dist/src/types/config.js +42 -0
- package/code-review-agent/dist/src/types/config.js.map +1 -0
- package/code-review-agent/dist/src/types/findings.d.ts +236 -0
- package/code-review-agent/dist/src/types/findings.d.ts.map +1 -0
- package/code-review-agent/dist/src/types/findings.js +64 -0
- package/code-review-agent/dist/src/types/findings.js.map +1 -0
- package/code-review-agent/package.json +36 -0
- package/code-review-agent/src/analyzer/engine.ts +374 -0
- package/code-review-agent/src/analyzer/intent.ts +49 -0
- package/code-review-agent/src/analyzer/semantic.ts +222 -0
- package/code-review-agent/src/context/assembler.ts +165 -0
- package/code-review-agent/src/context/file.ts +145 -0
- package/code-review-agent/src/context/project.ts +253 -0
- package/code-review-agent/src/graph/dependency.ts +116 -0
- package/code-review-agent/src/graph/resolver.ts +138 -0
- package/code-review-agent/src/index.ts +58 -0
- package/code-review-agent/src/llm/anthropic.ts +106 -0
- package/code-review-agent/src/llm/claude-cli.ts +188 -0
- package/code-review-agent/src/llm/openai.ts +95 -0
- package/code-review-agent/src/llm/provider.ts +33 -0
- package/code-review-agent/src/llm/router.ts +86 -0
- package/code-review-agent/src/llm/schemas.ts +125 -0
- package/code-review-agent/src/types/analysis.ts +62 -0
- package/code-review-agent/src/types/config.ts +72 -0
- package/code-review-agent/src/types/findings.ts +81 -0
- package/code-review-agent/tests/analyzer/engine.test.ts +194 -0
- package/code-review-agent/tests/analyzer/intent.test.ts +76 -0
- package/code-review-agent/tests/analyzer/semantic.test.ts +131 -0
- package/code-review-agent/tests/context/file.test.ts +21 -0
- package/code-review-agent/tests/context/project.test.ts +20 -0
- package/code-review-agent/tests/fixtures/safe-build-tool/README.md +19 -0
- package/code-review-agent/tests/fixtures/safe-build-tool/builder.js +52 -0
- package/code-review-agent/tests/fixtures/safe-file-manager/README.md +16 -0
- package/code-review-agent/tests/fixtures/safe-file-manager/organizer.py +70 -0
- package/code-review-agent/tests/fixtures/vuln-api-server/README.md +17 -0
- package/code-review-agent/tests/fixtures/vuln-api-server/server.js +52 -0
- package/code-review-agent/tests/fixtures/vuln-ecommerce/README.md +18 -0
- package/code-review-agent/tests/fixtures/vuln-ecommerce/checkout.js +63 -0
- package/code-review-agent/tests/graph/dependency.test.ts +136 -0
- package/code-review-agent/tests/helpers/mock-provider.ts +48 -0
- package/code-review-agent/tests/llm/claude-cli.test.ts +251 -0
- package/code-review-agent/tests/llm/router.test.ts +77 -0
- package/code-review-agent/tests/llm/schemas.test.ts +142 -0
- package/code-review-agent/tsconfig.json +20 -0
- package/code-review-agent/vitest.config.ts +11 -0
- package/index.js +18 -18
- package/openclaw.plugin.json +2 -2
- package/package.json +13 -3
- package/server.json +3 -3
- package/src/cli/init-hooks.js +3 -3
- package/src/cli/init.js +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SemanticAnalyzer } from '../../src/analyzer/semantic.js';
|
|
3
|
+
import { MockLLMProvider } from '../helpers/mock-provider.js';
|
|
4
|
+
import type { FileContext, ProjectContext } from '../../src/types/analysis.js';
|
|
5
|
+
import type { IntentProfile } from '../../src/types/findings.js';
|
|
6
|
+
|
|
7
|
+
const mockIntent: IntentProfile = {
|
|
8
|
+
purpose: 'REST API for user management',
|
|
9
|
+
expectedBehaviors: ['Handle HTTP requests', 'Read/write database'],
|
|
10
|
+
unexpectedBehaviors: ['Execute system commands', 'Eval user input'],
|
|
11
|
+
framework: 'express',
|
|
12
|
+
riskDomain: 'web-api',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const mockProject: ProjectContext = {
|
|
16
|
+
readme: '# API Server',
|
|
17
|
+
packageMeta: { name: 'api', dependencies: { express: '^4.0.0' } },
|
|
18
|
+
directoryTree: 'src/\n server.js',
|
|
19
|
+
envVars: [],
|
|
20
|
+
hasDockerfile: false,
|
|
21
|
+
hasCI: false,
|
|
22
|
+
language: 'javascript/typescript',
|
|
23
|
+
framework: 'express',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const mockFileContext: FileContext = {
|
|
27
|
+
filePath: 'server.js',
|
|
28
|
+
content: 'const x = eval(req.query.code);',
|
|
29
|
+
language: 'javascript',
|
|
30
|
+
lineCount: 1,
|
|
31
|
+
imports: ['express'],
|
|
32
|
+
importedBy: [],
|
|
33
|
+
siblingFiles: [],
|
|
34
|
+
isTestFile: false,
|
|
35
|
+
isConfigFile: false,
|
|
36
|
+
isGenerated: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const mockAnalysisResponse = {
|
|
40
|
+
findings: [
|
|
41
|
+
{
|
|
42
|
+
title: 'eval() on user input',
|
|
43
|
+
severity: 'critical' as const,
|
|
44
|
+
category: 'security' as const,
|
|
45
|
+
location: { file: 'server.js', startLine: 1, endLine: 1 },
|
|
46
|
+
reasoning: 'eval() with user-controlled input allows arbitrary code execution',
|
|
47
|
+
intentAlignment: 'violates-intent' as const,
|
|
48
|
+
confidence: 0.95,
|
|
49
|
+
suggestedAction: 'Use a safe parser or whitelist approach instead of eval',
|
|
50
|
+
cwe: 'CWE-94',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mockTriageResponse = {
|
|
56
|
+
action: 'analyze' as const,
|
|
57
|
+
reason: 'File handles HTTP requests and uses eval',
|
|
58
|
+
areasOfInterest: [{ startLine: 1, endLine: 1, reason: 'eval() call' }],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('SemanticAnalyzer', () => {
|
|
62
|
+
it('analyzes a file and returns findings', async () => {
|
|
63
|
+
const analysisProvider = new MockLLMProvider({
|
|
64
|
+
file_analysis: mockAnalysisResponse,
|
|
65
|
+
});
|
|
66
|
+
const triageProvider = new MockLLMProvider({
|
|
67
|
+
triage_decision: mockTriageResponse,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const analyzer = new SemanticAnalyzer(analysisProvider, triageProvider);
|
|
71
|
+
const result = await analyzer.analyzeFile(mockIntent, mockProject, mockFileContext);
|
|
72
|
+
|
|
73
|
+
expect(result.findings).toHaveLength(1);
|
|
74
|
+
expect(result.findings[0].title).toBe('eval() on user input');
|
|
75
|
+
expect(result.findings[0].severity).toBe('critical');
|
|
76
|
+
expect(result.findings[0].intentAlignment).toBe('violates-intent');
|
|
77
|
+
expect(result.findings[0].confidence).toBe(0.95);
|
|
78
|
+
expect(result.tokensUsed).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('triages a file and returns decision', async () => {
|
|
82
|
+
const analysisProvider = new MockLLMProvider({});
|
|
83
|
+
const triageProvider = new MockLLMProvider({
|
|
84
|
+
triage_decision: mockTriageResponse,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const analyzer = new SemanticAnalyzer(analysisProvider, triageProvider);
|
|
88
|
+
const decision = await analyzer.triageFile(mockProject, mockFileContext);
|
|
89
|
+
|
|
90
|
+
expect(decision.action).toBe('analyze');
|
|
91
|
+
expect(decision.areasOfInterest).toHaveLength(1);
|
|
92
|
+
expect(triageProvider.structuredCalls[0]?.messages[0]?.content).toContain('UNTRUSTED INPUT');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns skip decision for safe files', async () => {
|
|
96
|
+
const skipResponse = {
|
|
97
|
+
action: 'skip' as const,
|
|
98
|
+
reason: 'Test file with no security-relevant code',
|
|
99
|
+
areasOfInterest: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const analysisProvider = new MockLLMProvider({});
|
|
103
|
+
const triageProvider = new MockLLMProvider({
|
|
104
|
+
triage_decision: skipResponse,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const analyzer = new SemanticAnalyzer(analysisProvider, triageProvider);
|
|
108
|
+
const decision = await analyzer.triageFile(mockProject, {
|
|
109
|
+
...mockFileContext,
|
|
110
|
+
filePath: 'test.test.js',
|
|
111
|
+
isTestFile: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(decision.action).toBe('skip');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('sets correct file path on findings', async () => {
|
|
118
|
+
const analysisProvider = new MockLLMProvider({
|
|
119
|
+
file_analysis: mockAnalysisResponse,
|
|
120
|
+
});
|
|
121
|
+
const triageProvider = new MockLLMProvider({});
|
|
122
|
+
|
|
123
|
+
const analyzer = new SemanticAnalyzer(analysisProvider, triageProvider);
|
|
124
|
+
const result = await analyzer.analyzeFile(mockIntent, mockProject, {
|
|
125
|
+
...mockFileContext,
|
|
126
|
+
filePath: 'src/routes/users.js',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.findings[0].location.file).toBe('src/routes/users.js');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isTestFile } from '../../src/context/file.js';
|
|
3
|
+
|
|
4
|
+
describe('isTestFile', () => {
|
|
5
|
+
it('matches top-level test directories on POSIX paths', () => {
|
|
6
|
+
expect(isTestFile('tests/helpers/mock-provider.ts')).toBe(true);
|
|
7
|
+
expect(isTestFile('test/unit/example.js')).toBe(true);
|
|
8
|
+
expect(isTestFile('__tests__/parser.test.ts')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('matches top-level test directories on Windows paths', () => {
|
|
12
|
+
expect(isTestFile('tests\\helpers\\mock-provider.ts')).toBe(true);
|
|
13
|
+
expect(isTestFile('test\\unit\\example.js')).toBe(true);
|
|
14
|
+
expect(isTestFile('__tests__\\parser.test.ts')).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('does not classify regular source files as tests', () => {
|
|
18
|
+
expect(isTestFile('src/server.js')).toBe(false);
|
|
19
|
+
expect(isTestFile('fixtures/vuln-api-server/server.js')).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { buildProjectContext } from '../../src/context/project.js';
|
|
6
|
+
|
|
7
|
+
describe('buildProjectContext', () => {
|
|
8
|
+
it('detects language from nested source files when manifests are absent', () => {
|
|
9
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-project-'));
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
12
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'server.js'), 'console.log(1);');
|
|
13
|
+
|
|
14
|
+
const context = buildProjectContext(tmpDir);
|
|
15
|
+
expect(context.language).toBe('javascript/typescript');
|
|
16
|
+
} finally {
|
|
17
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Build Runner
|
|
2
|
+
|
|
3
|
+
A JavaScript build tool that compiles and bundles project assets. Runs predefined build steps like TypeScript compilation, CSS processing, and asset copying.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node builder.js build
|
|
9
|
+
node builder.js clean
|
|
10
|
+
node builder.js watch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Build Steps
|
|
14
|
+
|
|
15
|
+
1. Clean dist/ directory
|
|
16
|
+
2. Compile TypeScript
|
|
17
|
+
3. Process CSS with PostCSS
|
|
18
|
+
4. Copy static assets
|
|
19
|
+
5. Generate source maps
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const DIST = path.join(__dirname, 'dist');
|
|
6
|
+
|
|
7
|
+
const BUILD_STEPS = [
|
|
8
|
+
{ name: 'typescript', cmd: 'npx', args: ['tsc', '--outDir', DIST] },
|
|
9
|
+
{ name: 'css', cmd: 'npx', args: ['postcss', 'src/styles/*.css', '--dir', path.join(DIST, 'css')] },
|
|
10
|
+
{ name: 'assets', cmd: 'cp', args: ['-r', 'public/', path.join(DIST, 'public')] },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function clean() {
|
|
14
|
+
if (fs.existsSync(DIST)) {
|
|
15
|
+
fs.rmSync(DIST, { recursive: true });
|
|
16
|
+
console.log('Cleaned dist/');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function runStep(step) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
console.log(`Running: ${step.name}`);
|
|
23
|
+
execFile(step.cmd, step.args, { cwd: __dirname }, (error, stdout, stderr) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
console.error(`${step.name} failed:`, stderr);
|
|
26
|
+
reject(error);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(`${step.name} done:`, stdout.trim());
|
|
30
|
+
resolve(stdout);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function build() {
|
|
36
|
+
fs.mkdirSync(DIST, { recursive: true });
|
|
37
|
+
for (const step of BUILD_STEPS) {
|
|
38
|
+
await runStep(step);
|
|
39
|
+
}
|
|
40
|
+
console.log('Build complete!');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const command = process.argv[2] || 'build';
|
|
44
|
+
if (command === 'clean') {
|
|
45
|
+
clean();
|
|
46
|
+
} else if (command === 'build') {
|
|
47
|
+
clean();
|
|
48
|
+
build().catch((err) => {
|
|
49
|
+
console.error('Build failed:', err.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# File Organizer
|
|
2
|
+
|
|
3
|
+
A Python utility that organizes files in a directory by extension. It moves files into categorized subdirectories (images/, docs/, code/, etc.) based on their file type.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python organizer.py /path/to/messy/directory
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Sorts files by extension into categorized folders
|
|
14
|
+
- Handles duplicates by appending a number
|
|
15
|
+
- Supports dry-run mode with --dry-run flag
|
|
16
|
+
- Logs all operations to organizer.log
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""File organizer - sorts files into categorized directories."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import shutil
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logging.basicConfig(filename='organizer.log', level=logging.INFO)
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
CATEGORIES = {
|
|
12
|
+
'images': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg'],
|
|
13
|
+
'docs': ['.pdf', '.doc', '.docx', '.txt', '.md', '.csv'],
|
|
14
|
+
'code': ['.py', '.js', '.ts', '.java', '.go', '.rs'],
|
|
15
|
+
'archives': ['.zip', '.tar', '.gz', '.rar', '.7z'],
|
|
16
|
+
'media': ['.mp3', '.mp4', '.avi', '.mkv', '.wav'],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_category(filename):
|
|
21
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
22
|
+
for category, extensions in CATEGORIES.items():
|
|
23
|
+
if ext in extensions:
|
|
24
|
+
return category
|
|
25
|
+
return 'other'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def organize(directory, dry_run=False):
|
|
29
|
+
if not os.path.isdir(directory):
|
|
30
|
+
logger.error(f"Not a directory: {directory}")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
for filename in os.listdir(directory):
|
|
34
|
+
filepath = os.path.join(directory, filename)
|
|
35
|
+
if not os.path.isfile(filepath):
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
category = get_category(filename)
|
|
39
|
+
target_dir = os.path.join(directory, category)
|
|
40
|
+
|
|
41
|
+
if not dry_run:
|
|
42
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
43
|
+
target_path = os.path.join(target_dir, filename)
|
|
44
|
+
|
|
45
|
+
# Handle duplicates
|
|
46
|
+
counter = 1
|
|
47
|
+
base, ext = os.path.splitext(filename)
|
|
48
|
+
while os.path.exists(target_path):
|
|
49
|
+
target_path = os.path.join(target_dir, f"{base}_{counter}{ext}")
|
|
50
|
+
counter += 1
|
|
51
|
+
|
|
52
|
+
shutil.move(filepath, target_path)
|
|
53
|
+
logger.info(f"Moved {filename} -> {category}/")
|
|
54
|
+
else:
|
|
55
|
+
logger.info(f"[DRY RUN] Would move {filename} -> {category}/")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cleanup_empty_dirs(directory):
|
|
59
|
+
for dirpath, dirnames, filenames in os.walk(directory, topdown=False):
|
|
60
|
+
if not filenames and not dirnames:
|
|
61
|
+
os.rmdir(dirpath)
|
|
62
|
+
logger.info(f"Removed empty directory: {dirpath}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
target = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
67
|
+
dry_run = '--dry-run' in sys.argv
|
|
68
|
+
organize(target, dry_run)
|
|
69
|
+
if not dry_run:
|
|
70
|
+
cleanup_empty_dirs(target)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# User API Server
|
|
2
|
+
|
|
3
|
+
A REST API server for managing user accounts. Handles user registration, authentication, and profile management.
|
|
4
|
+
|
|
5
|
+
## Endpoints
|
|
6
|
+
|
|
7
|
+
- POST /api/register - Create new account
|
|
8
|
+
- POST /api/login - Authenticate user
|
|
9
|
+
- GET /api/profile/:id - Get user profile
|
|
10
|
+
- PUT /api/profile/:id - Update profile
|
|
11
|
+
- GET /api/search - Search users
|
|
12
|
+
|
|
13
|
+
## Stack
|
|
14
|
+
|
|
15
|
+
- Node.js + Express
|
|
16
|
+
- SQLite database
|
|
17
|
+
- JWT authentication
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const sqlite3 = require('sqlite3');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const app = express();
|
|
5
|
+
|
|
6
|
+
app.use(express.json());
|
|
7
|
+
|
|
8
|
+
const db = new sqlite3.Database('./users.db');
|
|
9
|
+
|
|
10
|
+
// User registration
|
|
11
|
+
app.post('/api/register', (req, res) => {
|
|
12
|
+
const { username, email, password } = req.body;
|
|
13
|
+
// SQL injection - string concatenation instead of parameterized query
|
|
14
|
+
const query = `INSERT INTO users (username, email, password) VALUES ('${username}', '${email}', '${password}')`;
|
|
15
|
+
db.run(query, (err) => {
|
|
16
|
+
if (err) return res.status(500).json({ error: err.message });
|
|
17
|
+
res.json({ success: true });
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// User search - eval injection
|
|
22
|
+
app.get('/api/search', (req, res) => {
|
|
23
|
+
const { filter } = req.query;
|
|
24
|
+
// eval on user input - command injection
|
|
25
|
+
const filterFn = eval(`(user) => ${filter}`);
|
|
26
|
+
db.all('SELECT * FROM users', (err, users) => {
|
|
27
|
+
if (err) return res.status(500).json({ error: err.message });
|
|
28
|
+
const filtered = users.filter(filterFn);
|
|
29
|
+
res.json(filtered);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Profile picture upload - path traversal
|
|
34
|
+
app.post('/api/upload', (req, res) => {
|
|
35
|
+
const { filename, data } = req.body;
|
|
36
|
+
// Arbitrary file write - no path sanitization
|
|
37
|
+
fs.writeFile(`./uploads/${filename}`, Buffer.from(data, 'base64'), (err) => {
|
|
38
|
+
if (err) return res.status(500).json({ error: err.message });
|
|
39
|
+
res.json({ path: `/uploads/${filename}` });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Debug endpoint - exposes server info
|
|
44
|
+
app.get('/api/debug', (req, res) => {
|
|
45
|
+
res.json({
|
|
46
|
+
env: process.env,
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
memory: process.memoryUsage(),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
app.listen(3000);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# E-Commerce Checkout
|
|
2
|
+
|
|
3
|
+
A Node.js e-commerce checkout service. Handles shopping cart management, price calculation, payment processing, and order confirmation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Shopping cart with add/remove/update
|
|
8
|
+
- Discount code validation
|
|
9
|
+
- Payment gateway integration (Stripe)
|
|
10
|
+
- Order confirmation emails
|
|
11
|
+
- Inventory management
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
- POST /cart/add - Add item to cart
|
|
16
|
+
- POST /checkout - Process payment
|
|
17
|
+
- GET /order/:id - Get order status
|
|
18
|
+
- POST /discount/validate - Validate discount code
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const app = express();
|
|
3
|
+
|
|
4
|
+
app.use(express.json());
|
|
5
|
+
|
|
6
|
+
const carts = {};
|
|
7
|
+
|
|
8
|
+
// Add to cart - prototype pollution via merge
|
|
9
|
+
app.post('/cart/add', (req, res) => {
|
|
10
|
+
const { userId, item } = req.body;
|
|
11
|
+
if (!carts[userId]) carts[userId] = { items: [] };
|
|
12
|
+
// Deep merge without prototype pollution protection
|
|
13
|
+
merge(carts[userId], item);
|
|
14
|
+
res.json(carts[userId]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function merge(target, source) {
|
|
18
|
+
for (const key in source) {
|
|
19
|
+
if (typeof source[key] === 'object' && source[key] !== null) {
|
|
20
|
+
if (!target[key]) target[key] = {};
|
|
21
|
+
merge(target[key], source[key]);
|
|
22
|
+
} else {
|
|
23
|
+
target[key] = source[key];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Apply discount - no auth check, price manipulation
|
|
30
|
+
app.post('/discount/validate', (req, res) => {
|
|
31
|
+
const { code, cartTotal } = req.body;
|
|
32
|
+
// User controls the cart total - should be server-side calculation
|
|
33
|
+
const discounts = { SAVE10: 0.1, SAVE50: 0.5, EMPLOYEE: 1.0 };
|
|
34
|
+
const discount = discounts[code];
|
|
35
|
+
if (discount) {
|
|
36
|
+
res.json({ newTotal: cartTotal * (1 - discount), valid: true });
|
|
37
|
+
} else {
|
|
38
|
+
res.json({ valid: false });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Checkout - open redirect in success URL
|
|
43
|
+
app.post('/checkout', (req, res) => {
|
|
44
|
+
const { successUrl } = req.body;
|
|
45
|
+
// Process payment...
|
|
46
|
+
// Open redirect - user controls redirect destination
|
|
47
|
+
res.redirect(successUrl);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Order lookup - no authorization, IDOR
|
|
51
|
+
app.get('/order/:id', (req, res) => {
|
|
52
|
+
// No check that requesting user owns this order
|
|
53
|
+
const order = getOrder(req.params.id);
|
|
54
|
+
if (!order) return res.status(404).json({ error: 'Not found' });
|
|
55
|
+
res.json(order);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function getOrder(id) {
|
|
59
|
+
// Stub
|
|
60
|
+
return { id, items: [], total: 0, status: 'completed' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
app.listen(3000);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { DependencyGraphBuilder } from '../../src/graph/dependency.js';
|
|
6
|
+
|
|
7
|
+
describe('DependencyGraphBuilder', () => {
|
|
8
|
+
it('builds a graph from fixture files', () => {
|
|
9
|
+
const fixtureDir = path.resolve(__dirname, '../fixtures/vuln-api-server');
|
|
10
|
+
const builder = new DependencyGraphBuilder(fixtureDir);
|
|
11
|
+
const graph = builder.build(['server.js']);
|
|
12
|
+
|
|
13
|
+
expect(graph.nodes.size).toBeGreaterThan(0);
|
|
14
|
+
expect(graph.entryPoints).toEqual(['server.js']);
|
|
15
|
+
|
|
16
|
+
const serverNode = graph.nodes.get('server.js');
|
|
17
|
+
expect(serverNode).toBeTruthy();
|
|
18
|
+
expect(serverNode!.file).toBe('server.js');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('resolves local imports', () => {
|
|
22
|
+
// Create a temp directory with two files that import each other
|
|
23
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-test-'));
|
|
24
|
+
try {
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
path.join(tmpDir, 'main.js'),
|
|
27
|
+
"import { helper } from './helper.js';\nconsole.log(helper());",
|
|
28
|
+
);
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
path.join(tmpDir, 'helper.js'),
|
|
31
|
+
"export function helper() { return 'hello'; }",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
35
|
+
const graph = builder.build(['main.js']);
|
|
36
|
+
|
|
37
|
+
expect(graph.nodes.size).toBe(2);
|
|
38
|
+
|
|
39
|
+
const mainNode = graph.nodes.get('main.js');
|
|
40
|
+
expect(mainNode?.imports).toContain('helper.js');
|
|
41
|
+
|
|
42
|
+
const helperNode = graph.nodes.get('helper.js');
|
|
43
|
+
expect(helperNode?.importedBy).toContain('main.js');
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles cycles without infinite loop', () => {
|
|
50
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-cycle-'));
|
|
51
|
+
try {
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(tmpDir, 'a.js'),
|
|
54
|
+
"import './b.js';",
|
|
55
|
+
);
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
path.join(tmpDir, 'b.js'),
|
|
58
|
+
"import './a.js';",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
62
|
+
const graph = builder.build(['a.js']);
|
|
63
|
+
|
|
64
|
+
expect(graph.nodes.size).toBe(2);
|
|
65
|
+
// Should complete without hanging
|
|
66
|
+
} finally {
|
|
67
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('getImporters returns files that import a given file', () => {
|
|
72
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-importers-'));
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(path.join(tmpDir, 'index.js'), "import './utils.js';");
|
|
75
|
+
fs.writeFileSync(path.join(tmpDir, 'utils.js'), "export const x = 1;");
|
|
76
|
+
|
|
77
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
78
|
+
builder.build(['index.js']);
|
|
79
|
+
|
|
80
|
+
expect(builder.getImporters('utils.js')).toContain('index.js');
|
|
81
|
+
expect(builder.getImportees('index.js')).toContain('utils.js');
|
|
82
|
+
} finally {
|
|
83
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ignores non-local imports', () => {
|
|
88
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-external-'));
|
|
89
|
+
try {
|
|
90
|
+
fs.writeFileSync(
|
|
91
|
+
path.join(tmpDir, 'app.js'),
|
|
92
|
+
"import express from 'express';\nimport chalk from 'chalk';",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
96
|
+
const graph = builder.build(['app.js']);
|
|
97
|
+
|
|
98
|
+
const appNode = graph.nodes.get('app.js');
|
|
99
|
+
expect(appNode?.imports).toHaveLength(0);
|
|
100
|
+
} finally {
|
|
101
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('includes barrel re-export edges in the graph', () => {
|
|
106
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-barrel-'));
|
|
107
|
+
try {
|
|
108
|
+
fs.writeFileSync(path.join(tmpDir, 'app.js'), "import { value } from './index.js';");
|
|
109
|
+
fs.writeFileSync(path.join(tmpDir, 'index.js'), "export * from './lib.js';");
|
|
110
|
+
fs.writeFileSync(path.join(tmpDir, 'lib.js'), 'export const value = 1;');
|
|
111
|
+
|
|
112
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
113
|
+
const graph = builder.build(['app.js']);
|
|
114
|
+
|
|
115
|
+
expect(graph.nodes.get('index.js')?.imports).toContain('lib.js');
|
|
116
|
+
expect(graph.nodes.get('lib.js')?.importedBy).toContain('index.js');
|
|
117
|
+
} finally {
|
|
118
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('keeps supported non-JS entry files in the graph', () => {
|
|
123
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-java-'));
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(path.join(tmpDir, 'Main.java'), 'class Main {}');
|
|
126
|
+
|
|
127
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
128
|
+
const graph = builder.build(['Main.java']);
|
|
129
|
+
|
|
130
|
+
expect(graph.entryPoints).toEqual(['Main.java']);
|
|
131
|
+
expect(graph.nodes.get('Main.java')?.file).toBe('Main.java');
|
|
132
|
+
} finally {
|
|
133
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { ChatMessage, LLMProvider } from '../../src/llm/provider.js';
|
|
3
|
+
|
|
4
|
+
export class MockLLMProvider implements LLMProvider {
|
|
5
|
+
readonly modelId: string;
|
|
6
|
+
readonly providerName = 'mock';
|
|
7
|
+
|
|
8
|
+
private responses: Map<string, unknown>;
|
|
9
|
+
public chatCalls: Array<{ messages: ChatMessage[] }> = [];
|
|
10
|
+
public structuredCalls: Array<{ messages: ChatMessage[]; schemaName: string }> = [];
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
responses: Record<string, unknown>,
|
|
14
|
+
modelId = 'mock-model',
|
|
15
|
+
) {
|
|
16
|
+
this.responses = new Map(Object.entries(responses));
|
|
17
|
+
this.modelId = modelId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async chat(messages: ChatMessage[]): Promise<string> {
|
|
21
|
+
this.chatCalls.push({ messages });
|
|
22
|
+
return 'mock response';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async chatStructured<T>(
|
|
26
|
+
messages: ChatMessage[],
|
|
27
|
+
schema: z.ZodType<T>,
|
|
28
|
+
schemaName: string,
|
|
29
|
+
): Promise<T> {
|
|
30
|
+
this.structuredCalls.push({ messages, schemaName });
|
|
31
|
+
|
|
32
|
+
const response = this.responses.get(schemaName);
|
|
33
|
+
if (!response) {
|
|
34
|
+
throw new Error(`No mock response for schema: ${schemaName}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = schema.safeParse(response);
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
throw new Error(`Mock response failed validation for ${schemaName}: ${result.error.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result.data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
countTokens(text: string): number {
|
|
46
|
+
return Math.ceil(text.length / 4);
|
|
47
|
+
}
|
|
48
|
+
}
|