opensecurity 0.1.0 → 0.2.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.
Files changed (42) hide show
  1. package/README.md +156 -30
  2. package/assets/grammars/README.md +9 -0
  3. package/assets/grammars/tree-sitter-c-sharp.wasm +0 -0
  4. package/assets/grammars/tree-sitter-c.wasm +0 -0
  5. package/assets/grammars/tree-sitter-cpp.wasm +0 -0
  6. package/assets/grammars/tree-sitter-go.wasm +0 -0
  7. package/assets/grammars/tree-sitter-java.wasm +0 -0
  8. package/assets/grammars/tree-sitter-kotlin.wasm +0 -0
  9. package/assets/grammars/tree-sitter-php.wasm +0 -0
  10. package/assets/grammars/tree-sitter-python.wasm +0 -0
  11. package/assets/grammars/tree-sitter-ruby.wasm +0 -0
  12. package/assets/grammars/tree-sitter-rust.wasm +0 -0
  13. package/assets/grammars/tree-sitter-swift.wasm +0 -0
  14. package/dist/adapters/bandit.js +41 -0
  15. package/dist/adapters/brakeman.js +41 -0
  16. package/dist/adapters/gosec.js +49 -0
  17. package/dist/adapters/languages.js +29 -0
  18. package/dist/adapters/runner.js +46 -0
  19. package/dist/adapters/semgrep.js +59 -0
  20. package/dist/adapters/types.js +1 -0
  21. package/dist/adapters/utils.js +52 -0
  22. package/dist/analysis/infraPatterns.js +196 -0
  23. package/dist/analysis/universalPatterns.js +56 -0
  24. package/dist/cli.js +15 -1
  25. package/dist/config.js +2 -1
  26. package/dist/native/languages.js +211 -0
  27. package/dist/native/loader.js +61 -0
  28. package/dist/native/rules.js +14 -0
  29. package/dist/native/taint.js +225 -0
  30. package/dist/scan.js +207 -0
  31. package/package.json +21 -2
  32. package/rules/taint/c.json +47 -0
  33. package/rules/taint/cpp.json +47 -0
  34. package/rules/taint/csharp.json +99 -0
  35. package/rules/taint/go.json +86 -0
  36. package/rules/taint/java.json +101 -0
  37. package/rules/taint/kotlin.json +86 -0
  38. package/rules/taint/php.json +100 -0
  39. package/rules/taint/python.json +108 -0
  40. package/rules/taint/ruby.json +101 -0
  41. package/rules/taint/rust.json +86 -0
  42. package/rules/taint/swift.json +86 -0
package/README.md CHANGED
@@ -1,56 +1,74 @@
1
1
  # OpenSecurity
2
2
 
3
- OpenSecurity is an open-source CLI for scanning codebases for security risks, with first-class static analysis for JavaScript/TypeScript and optional AI scanning for broader files.
4
- It combines:
3
+ OpenSecurity is an open-source CLI that scans entire repositories for security risks across application code, infrastructure/config files, and dependencies. It combines fast local analysis with optional AI scanning to keep coverage broad but practical.
5
4
 
6
- - Static analysis with AST-based taint rules (OWASP-focused).
7
- - Dependency scanning with CVE lookup (local cache or API).
8
- - Optional AI-assisted scanning for deeper findings.
5
+ At a high level, it uses multiple engines:
9
6
 
10
- ## Project Status
11
-
12
- Active. This repo is maintained and intended for open-source use. Contributions are welcome.
7
+ - **JS/TS native AST + taint + patterns**
8
+ - **Tree‑sitter native taint** for Python/Go/Java/C#/Ruby/PHP/Rust/Kotlin/Swift/C/C++
9
+ - **External adapters** (Bandit, gosec, Brakeman, Semgrep) when installed
10
+ - **Infra/config patterns** (Dockerfile, Kubernetes/Helm, Terraform, YAML)
11
+ - **Dependency CVE scanning** for npm/PyPI
12
+ - **Optional AI scan** across text files (`--no-ai` to disable)
13
13
 
14
+ Universal patterns are heuristic (fast but shallow). Native AST/taint is a baseline multi‑lang engine and does not replace deep, language‑specific SAST.
14
15
  ## Scope
15
16
 
16
- - Target languages for static analysis: JavaScript and TypeScript.
17
- - Dependency scanning: npm and PyPI manifests.
18
- - AI scanning is optional and requires an API key (defaults to scanning all text files).
17
+ - JS/TS native AST + taint + patterns
18
+ - Tree‑sitter native taint for Python/Go/Java/C#/Ruby/PHP/Rust/Kotlin/Swift/C/C++
19
+ - Optional adapters: Bandit, gosec, Brakeman, Semgrep
20
+ - Infra/config patterns: Dockerfile, Kubernetes/Helm YAML, Terraform, generic YAML
21
+ - Dependency scanning: npm and PyPI manifests
22
+ - AI scanning is optional but **recommended** for deeper coverage; it requires an API key and is skipped when none is configured
19
23
 
20
24
  ## Non-Goals
21
25
 
22
26
  - This tool is not a full SAST replacement or compliance scanner.
23
27
  - It does not execute or sandbox code.
24
28
  - It does not guarantee complete coverage of all vulnerabilities.
29
+ - It does not replace specialized commercial scanners (e.g., Codex Security, Claude Security).
30
+ - For compliance-grade coverage, use dedicated SAST/compliance tooling.
31
+ - Use it as a complementary signal, not a single source of truth.
25
32
 
26
- ## Quick Start
33
+ ## Install(recommended)
34
+ Node.js 20+ is required.
27
35
 
28
36
  ```bash
29
- npm install
30
- npm run dev -- scan --dry-run
37
+ npm i opensecurity
31
38
  ```
32
39
 
33
- Build the CLI:
40
+ Package: `https://www.npmjs.com/package/opensecurity`
41
+
42
+ No build step is required when installing from npm.
43
+
44
+ ## Quick Start
45
+
46
+ 1) Configure API key (optional, only if you want AI scanning):
34
47
 
35
48
  ```bash
36
- npm run build
37
- ./dist/cli.js scan --dry-run
49
+ opensecurity login --mode api_key --provider openai
38
50
  ```
39
51
 
40
- ## Install
52
+ 2) Run a scan:
53
+
54
+ ```bash
55
+ opensecurity scan
56
+ ```
41
57
 
42
- From source:
58
+ 3) Common options:
43
59
 
44
60
  ```bash
45
- npm install
46
- npm run build
47
- npm link
48
- opensecurity scan --dry-run
61
+ opensecurity scan --diff-only
62
+ opensecurity scan --no-ai
63
+ opensecurity scan --provider anthropic --model claude-sonnet-4-20250514
49
64
  ```
50
65
 
66
+ If no API key is configured, AI scanning is skipped automatically.
67
+ If you only want local scanning, you can skip login and run `opensecurity scan --no-ai`.
68
+
51
69
  ## Supported Platforms
52
70
 
53
- - Node.js 20+
71
+ - Node.js 20+ is required.
54
72
  - macOS, Linux, Windows
55
73
 
56
74
  ## Features
@@ -58,11 +76,98 @@ opensecurity scan --dry-run
58
76
  - AST taint engine with configurable sources/sinks/sanitizers.
59
77
  - OWASP-aligned default rules (injection, SSRF, path traversal, XSS templates, SQLi).
60
78
  - Pattern-based detectors (hardcoded secrets, insecure crypto, unsafe deserialization).
79
+ - Optional external adapters for top languages (Bandit, gosec, Brakeman, Semgrep).
61
80
  - Dependency scanning for npm and PyPI (`package.json`, `package-lock.json`, `requirements.txt`).
62
81
  - Text, JSON, and SARIF output.
63
82
  - Optional AI scan (API key or OAuth flow) with multiple providers.
64
83
  - Configurable include/exclude filters and scan scope.
65
84
 
85
+ ## How It Works
86
+
87
+ 1. **File discovery**
88
+ - Walks the repository based on `include`/`exclude`.
89
+ - Optional `--diff-only` or `--path` to narrow scope.
90
+
91
+ 2. **Static analysis (JS/TS)**
92
+ - Parses JS/TS with Babel and runs taint rules (sources → sinks → sanitizers).
93
+ - Emits OWASP-aligned findings.
94
+
95
+ 3. **Pattern detectors (JS/TS)**
96
+ - Finds hardcoded secrets, insecure crypto, and unsafe deserialization.
97
+
98
+ 4. **AI scan (all text files by default)**
99
+ - Splits files into chunks and sends to the configured model.
100
+ - Optional multi‑agent batching with shared leader context.
101
+ - Optional per‑file cache to skip unchanged files.
102
+
103
+ 5. **Native AST/taint (Tree‑sitter)**
104
+ - Loads Tree‑sitter parsers (WASM default, native optional).
105
+ - Runs taint rules per language for core injection/path/SSRF/deserialization/XSS.
106
+
107
+ 6. **External language adapters**
108
+ - Runs optional tool adapters when installed (Bandit, gosec, Brakeman, Semgrep).
109
+ - Each adapter only runs if matching files exist and the tool is on PATH.
110
+
111
+ 7. **Infra/config patterns**
112
+ - Scans Dockerfile, Terraform, and Kubernetes/Helm YAML for risky defaults.
113
+
114
+ 8. **Dependency scan**
115
+ - Reads `package.json`, `package-lock.json`, and `requirements.txt`.
116
+ - Matches against CVE cache or API and adds recommendations.
117
+
118
+ 9. **Reporting**
119
+ - Outputs text, JSON, or SARIF.
120
+ - Optional `--fail-on`/`--fail-on-high` for CI gating.
121
+
122
+ ## External Adapters
123
+
124
+ OpenSecurity can run optional external tools when they are installed and found on `PATH`.
125
+
126
+ - `bandit` → Python
127
+ - `gosec` → Go
128
+ - `brakeman` → Ruby
129
+ - `semgrep` → Java, C#, PHP, Rust, Kotlin, Swift, C/C++
130
+
131
+ Adapters only run when matching files exist. Use `--disable-adapters` to skip or `--adapters` to whitelist.
132
+
133
+ ## Tree-sitter Grammars
134
+
135
+ Native AST/taint uses Tree‑sitter grammars. WASM grammars live in `assets/grammars`.
136
+
137
+ Build them locally:
138
+
139
+ ```bash
140
+ npm run build-grammars
141
+ ```
142
+
143
+ This repo ships prebuilt WASM grammars for the 10 native languages. If they are missing, OpenSecurity will try native Tree‑sitter bindings (if installed) and otherwise skip native taint for that language with a warning.
144
+
145
+ ## Native AST/Taint Matrix
146
+
147
+ | Language | AST/Taint (Tree‑sitter) | Rules |
148
+ | --- | --- | --- |
149
+ | Python | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Deserialization, Template XSS, Weak Crypto, Hardcoded Secret |
150
+ | Go | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Template XSS, Weak Crypto, Hardcoded Secret |
151
+ | Java | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Deserialization, Template XSS, Weak Crypto, Hardcoded Secret |
152
+ | C# | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Deserialization, Template XSS, Weak Crypto, Hardcoded Secret |
153
+ | Ruby | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Deserialization, Template XSS, Weak Crypto, Hardcoded Secret |
154
+ | PHP | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Deserialization, Template XSS, Weak Crypto, Hardcoded Secret |
155
+ | Rust | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Template XSS, Weak Crypto, Hardcoded Secret |
156
+ | Kotlin | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Template XSS, Weak Crypto, Hardcoded Secret |
157
+ | Swift | ✅ | SQLi, Command Injection, Path Traversal, SSRF, Template XSS, Weak Crypto, Hardcoded Secret |
158
+ | C/C++ | ✅ | Command Injection, Path Traversal, Weak Crypto, Hardcoded Secret |
159
+
160
+ Rule coverage is heuristic and may miss context; validate findings in your environment.
161
+
162
+ ## Infra/Config Coverage
163
+
164
+ Built-in infra checks include:
165
+
166
+ - Dockerfile: `USER root`, privileged flags/capabilities.
167
+ - Kubernetes/Helm YAML: `privileged`, `allowPrivilegeEscalation`, `readOnlyRootFilesystem:false`, `runAsNonRoot:false`, `runAsUser:0`, `seccompProfile: Unconfined`, `hostNetwork/hostPID/hostIPC`, `hostPath`.
168
+ - Terraform: public security group ingress, public ACLs, public S3 ACLs, disabled S3 public access block, public RDS instances.
169
+ - YAML: insecure TLS (`insecureSkipVerify`, `verify_ssl: false`).
170
+
66
171
  ## CLI
67
172
 
68
173
  ### `scan`
@@ -91,6 +196,14 @@ Common options:
91
196
  - `--ai-cache`: enable AI per-file cache (default)
92
197
  - `--no-ai-cache`: disable AI per-file cache
93
198
  - `--ai-cache-path <path>`: path to AI cache file
199
+ - `--native-taint`: enable native multi-language taint engine (default)
200
+ - `--no-native-taint`: disable native multi-language taint engine
201
+ - `--native-langs <list>`: comma-separated native languages (python,go,java,csharp,ruby,php,rust,kotlin,swift,c,cpp)
202
+ - `--native-cache`: enable native taint cache (default)
203
+ - `--no-native-cache`: disable native taint cache
204
+ - `--native-cache-path <path>`: path to native taint cache file
205
+ - `--adapters <list>`: comma-separated adapter ids (bandit,gosec,brakeman,semgrep)
206
+ - `--disable-adapters`: disable external static adapters
94
207
  - `--dependency-only`: only run dependency scan
95
208
  - `--no-ai`: disable AI scanning
96
209
  - `--dry-run`: list matched files without scanning
@@ -107,6 +220,12 @@ opensecurity scan --no-ai
107
220
  opensecurity scan --format sarif --sarif-output reports/opensecurity.sarif
108
221
  opensecurity scan --provider anthropic --model claude-sonnet-4-20250514
109
222
  opensecurity scan --dependency-only --simulate
223
+ opensecurity scan --ai-multi-agent --ai-batch-size 25 --ai-batch-depth 2
224
+ opensecurity scan --diff-only --diff-base main
225
+ opensecurity scan --path src/
226
+ opensecurity scan --adapters bandit,gosec
227
+ opensecurity scan --disable-adapters
228
+ opensecurity scan --native-langs python,go,java
110
229
  ```
111
230
 
112
231
  ### `login`
@@ -154,7 +273,15 @@ Project config: `.opensecurity.json`
154
273
  "cveApiUrl": "https://example.com/osv",
155
274
  "dataSensitivity": "medium",
156
275
  "maxChars": 4000,
157
- "concurrency": 2
276
+ "concurrency": 2,
277
+ "aiCache": true,
278
+ "aiCachePath": ".opensecurity/ai-cache.json",
279
+ "nativeTaint": true,
280
+ "nativeTaintLanguages": ["python", "go", "java", "csharp", "ruby", "php", "rust", "kotlin", "swift", "c", "cpp"],
281
+ "nativeTaintCache": true,
282
+ "nativeTaintCachePath": ".opensecurity/native-taint-cache.json",
283
+ "adapters": ["bandit", "gosec", "brakeman", "semgrep"],
284
+ "noAdapters": false
158
285
  }
159
286
  ```
160
287
 
@@ -201,10 +328,6 @@ Rule schema (simplified):
201
328
  - **JSON**: machine-readable, includes `schemaVersion`
202
329
  - **SARIF**: for CI and code scanning tools
203
330
 
204
- ## Language Support
205
-
206
- - Static analysis: JavaScript and TypeScript
207
- - Dependency scanning: npm and PyPI manifests
208
331
 
209
332
  ## Security Notes
210
333
 
@@ -255,11 +378,14 @@ If you discover a vulnerability:
255
378
 
256
379
  For questions or help, open a GitHub issue with clear reproduction steps.
257
380
 
258
- ## Development
381
+ ## Development (contributors)
259
382
 
260
383
  ```bash
261
384
  npm install
262
385
  npm run dev -- scan --dry-run
386
+ npm run build
387
+ npm link
388
+ opensecurity scan --dry-run
263
389
  npm test
264
390
  ```
265
391
 
@@ -0,0 +1,9 @@
1
+ # Tree-sitter WASM Grammars
2
+
3
+ This directory holds prebuilt Tree-sitter WASM grammar files used by the native multi-language taint engine.
4
+
5
+ Generate or refresh them with:
6
+
7
+ ```bash
8
+ npm run build-grammars
9
+ ```
@@ -0,0 +1,41 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { PYTHON_EXTS, matchesExtension } from "./languages.js";
4
+ import { commandExists, extractJsonFromOutput, normalizePath } from "./utils.js";
5
+ const execFileAsync = promisify(execFile);
6
+ function mapSeverity(level) {
7
+ const norm = (level ?? "").toLowerCase();
8
+ if (norm === "high")
9
+ return "high";
10
+ if (norm === "medium")
11
+ return "medium";
12
+ return "low";
13
+ }
14
+ export const banditAdapter = {
15
+ id: "bandit",
16
+ name: "Bandit",
17
+ languages: ["Python"],
18
+ matchFile: (filePath) => matchesExtension(filePath, PYTHON_EXTS),
19
+ isAvailable: () => commandExists("bandit"),
20
+ async run(context) {
21
+ const { cwd, relPaths } = context;
22
+ if (!relPaths.length)
23
+ return [];
24
+ const args = ["-f", "json", ...relPaths];
25
+ const { stdout, stderr } = await execFileAsync("bandit", args, {
26
+ cwd,
27
+ maxBuffer: 10 * 1024 * 1024
28
+ });
29
+ const json = extractJsonFromOutput(stdout, stderr);
30
+ const results = Array.isArray(json?.results) ? json.results : [];
31
+ return results.map((item) => ({
32
+ id: item.test_id ?? "bandit",
33
+ severity: mapSeverity(item.issue_severity),
34
+ title: item.test_name ?? item.issue_text ?? "Bandit issue",
35
+ description: item.issue_text ?? "Bandit issue detected.",
36
+ file: normalizePath(item.filename ?? "", cwd),
37
+ line: item.line_number ? Number(item.line_number) : undefined,
38
+ category: "code"
39
+ }));
40
+ }
41
+ };
@@ -0,0 +1,41 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { RUBY_EXTS, matchesExtension } from "./languages.js";
4
+ import { commandExists, extractJsonFromOutput, normalizePath } from "./utils.js";
5
+ const execFileAsync = promisify(execFile);
6
+ function mapSeverity(confidence) {
7
+ const norm = (confidence ?? "").toLowerCase();
8
+ if (norm === "high")
9
+ return "high";
10
+ if (norm === "medium")
11
+ return "medium";
12
+ if (norm === "low")
13
+ return "low";
14
+ return "medium";
15
+ }
16
+ export const brakemanAdapter = {
17
+ id: "brakeman",
18
+ name: "Brakeman",
19
+ languages: ["Ruby"],
20
+ matchFile: (filePath) => matchesExtension(filePath, RUBY_EXTS),
21
+ isAvailable: () => commandExists("brakeman"),
22
+ async run(context) {
23
+ const { cwd } = context;
24
+ const args = ["-f", "json", "-q"];
25
+ const { stdout, stderr } = await execFileAsync("brakeman", args, {
26
+ cwd,
27
+ maxBuffer: 10 * 1024 * 1024
28
+ });
29
+ const json = extractJsonFromOutput(stdout, stderr);
30
+ const warnings = Array.isArray(json?.warnings) ? json.warnings : [];
31
+ return warnings.map((item) => ({
32
+ id: item.warning_code ?? item.warning_type ?? "brakeman",
33
+ severity: mapSeverity(item.confidence),
34
+ title: item.warning_type ?? item.message ?? "Brakeman warning",
35
+ description: item.message ?? "Brakeman warning detected.",
36
+ file: normalizePath(item.file ?? "", cwd),
37
+ line: item.line ? Number(item.line) : undefined,
38
+ category: "code"
39
+ }));
40
+ }
41
+ };
@@ -0,0 +1,49 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import path from "node:path";
4
+ import { GO_EXTS, matchesExtension } from "./languages.js";
5
+ import { commandExists, extractJsonFromOutput, normalizePath, unique } from "./utils.js";
6
+ const execFileAsync = promisify(execFile);
7
+ function mapSeverity(level) {
8
+ const norm = (level ?? "").toLowerCase();
9
+ if (norm === "high")
10
+ return "high";
11
+ if (norm === "medium")
12
+ return "medium";
13
+ return "low";
14
+ }
15
+ function uniqueDirs(relPaths) {
16
+ return unique(relPaths.map((relPath) => {
17
+ const dir = path.dirname(relPath);
18
+ return dir === "." ? "." : dir;
19
+ }));
20
+ }
21
+ export const gosecAdapter = {
22
+ id: "gosec",
23
+ name: "gosec",
24
+ languages: ["Go"],
25
+ matchFile: (filePath) => matchesExtension(filePath, GO_EXTS),
26
+ isAvailable: () => commandExists("gosec"),
27
+ async run(context) {
28
+ const { cwd, relPaths } = context;
29
+ if (!relPaths.length)
30
+ return [];
31
+ const dirs = uniqueDirs(relPaths);
32
+ const args = ["-fmt", "json", ...dirs];
33
+ const { stdout, stderr } = await execFileAsync("gosec", args, {
34
+ cwd,
35
+ maxBuffer: 10 * 1024 * 1024
36
+ });
37
+ const json = extractJsonFromOutput(stdout, stderr);
38
+ const issues = Array.isArray(json?.Issues) ? json.Issues : Array.isArray(json?.issues) ? json.issues : [];
39
+ return issues.map((item) => ({
40
+ id: item.rule_id ?? item.rule ?? "gosec",
41
+ severity: mapSeverity(item.severity),
42
+ title: item.details ?? item.what ?? "gosec issue",
43
+ description: item.details ?? item.what ?? "gosec issue detected.",
44
+ file: normalizePath(item.file ?? item.filename ?? "", cwd),
45
+ line: item.line ? Number(item.line) : undefined,
46
+ category: "code"
47
+ }));
48
+ }
49
+ };
@@ -0,0 +1,29 @@
1
+ const normalizeExt = (ext) => ext.toLowerCase();
2
+ export const PYTHON_EXTS = new Set([".py", ".pyw"].map(normalizeExt));
3
+ export const GO_EXTS = new Set([".go"].map(normalizeExt));
4
+ export const RUBY_EXTS = new Set([".rb"].map(normalizeExt));
5
+ export const PHP_EXTS = new Set([".php", ".phtml", ".php5", ".php7", ".phps"].map(normalizeExt));
6
+ export const RUST_EXTS = new Set([".rs"].map(normalizeExt));
7
+ export const JAVA_EXTS = new Set([".java"].map(normalizeExt));
8
+ export const CSHARP_EXTS = new Set([".cs"].map(normalizeExt));
9
+ export const KOTLIN_EXTS = new Set([".kt", ".kts"].map(normalizeExt));
10
+ export const SWIFT_EXTS = new Set([".swift", ".m", ".mm"].map(normalizeExt));
11
+ export const C_EXTS = new Set([".c", ".h"].map(normalizeExt));
12
+ export const CPP_EXTS = new Set([".cpp", ".hpp", ".cc", ".cxx", ".hh", ".hxx"].map(normalizeExt));
13
+ export const SEMGREP_EXTS = new Set([
14
+ ...JAVA_EXTS,
15
+ ...CSHARP_EXTS,
16
+ ...PHP_EXTS,
17
+ ...RUST_EXTS,
18
+ ...KOTLIN_EXTS,
19
+ ...SWIFT_EXTS,
20
+ ...C_EXTS,
21
+ ...CPP_EXTS
22
+ ].map(normalizeExt));
23
+ export function matchesExtension(filePath, extensions) {
24
+ const idx = filePath.lastIndexOf(".");
25
+ if (idx === -1)
26
+ return false;
27
+ const ext = filePath.slice(idx).toLowerCase();
28
+ return extensions.has(ext);
29
+ }
@@ -0,0 +1,46 @@
1
+ import path from "node:path";
2
+ import { banditAdapter } from "./bandit.js";
3
+ import { brakemanAdapter } from "./brakeman.js";
4
+ import { gosecAdapter } from "./gosec.js";
5
+ import { semgrepAdapter } from "./semgrep.js";
6
+ export const ALL_ADAPTERS = [
7
+ banditAdapter,
8
+ gosecAdapter,
9
+ brakemanAdapter,
10
+ semgrepAdapter
11
+ ];
12
+ export function filterAdapters(allowList) {
13
+ if (!allowList || allowList.length === 0)
14
+ return [...ALL_ADAPTERS];
15
+ const normalized = new Set(allowList.map((item) => item.trim().toLowerCase()).filter(Boolean));
16
+ return ALL_ADAPTERS.filter((adapter) => normalized.has(adapter.id));
17
+ }
18
+ export async function runExternalAdapters(options) {
19
+ const selected = filterAdapters(options.allowList);
20
+ const warnings = [];
21
+ const findings = [];
22
+ for (const adapter of selected) {
23
+ const matching = options.files.filter((filePath) => adapter.matchFile(filePath));
24
+ if (!matching.length)
25
+ continue;
26
+ const available = await adapter.isAvailable();
27
+ if (!available) {
28
+ warnings.push(`Adapter "${adapter.id}" skipped: ${adapter.name} not found in PATH.`);
29
+ continue;
30
+ }
31
+ const relPaths = matching.map((filePath) => path.relative(options.cwd, filePath).split(path.sep).join("/"));
32
+ try {
33
+ const adapterFindings = await adapter.run({
34
+ cwd: options.cwd,
35
+ targetPaths: matching,
36
+ relPaths,
37
+ onWarning: (message) => warnings.push(message)
38
+ });
39
+ findings.push(...adapterFindings);
40
+ }
41
+ catch (err) {
42
+ warnings.push(`Adapter "${adapter.id}" failed: ${err?.message ?? err}`);
43
+ }
44
+ }
45
+ return { findings, warnings };
46
+ }
@@ -0,0 +1,59 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { SEMGREP_EXTS, matchesExtension } from "./languages.js";
4
+ import { commandExists, extractJsonFromOutput, normalizePath, unique } from "./utils.js";
5
+ const execFileAsync = promisify(execFile);
6
+ function mapSeverity(level) {
7
+ const norm = (level ?? "").toLowerCase();
8
+ if (norm === "critical")
9
+ return "critical";
10
+ if (norm === "error" || norm === "high")
11
+ return "high";
12
+ if (norm === "warning" || norm === "medium")
13
+ return "medium";
14
+ return "low";
15
+ }
16
+ function uniqueDirs(relPaths) {
17
+ return unique(relPaths.map((relPath) => {
18
+ const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : ".";
19
+ return dir === "" ? "." : dir;
20
+ }));
21
+ }
22
+ export const semgrepAdapter = {
23
+ id: "semgrep",
24
+ name: "Semgrep",
25
+ languages: [
26
+ "Java",
27
+ "C#",
28
+ "PHP",
29
+ "Rust",
30
+ "Kotlin",
31
+ "Swift",
32
+ "C/C++"
33
+ ],
34
+ matchFile: (filePath) => matchesExtension(filePath, SEMGREP_EXTS),
35
+ isAvailable: () => commandExists("semgrep"),
36
+ async run(context) {
37
+ const { cwd, relPaths } = context;
38
+ if (!relPaths.length)
39
+ return [];
40
+ const targets = relPaths.length > 200 ? uniqueDirs(relPaths) : unique(relPaths);
41
+ const args = ["--config", "auto", "--json", "--quiet", ...targets];
42
+ const { stdout, stderr } = await execFileAsync("semgrep", args, {
43
+ cwd,
44
+ maxBuffer: 20 * 1024 * 1024
45
+ });
46
+ const json = extractJsonFromOutput(stdout, stderr);
47
+ const results = Array.isArray(json?.results) ? json.results : [];
48
+ return results.map((item) => ({
49
+ id: item.check_id ?? "semgrep",
50
+ severity: mapSeverity(item.extra?.severity),
51
+ title: item.extra?.message ?? item.check_id ?? "Semgrep issue",
52
+ description: item.extra?.message ?? "Semgrep issue detected.",
53
+ file: normalizePath(item.path ?? "", cwd),
54
+ line: item.start?.line ? Number(item.start.line) : undefined,
55
+ column: item.start?.col ? Number(item.start.col) : undefined,
56
+ category: "code"
57
+ }));
58
+ }
59
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import path from "node:path";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ const execFileAsync = promisify(execFile);
5
+ export async function commandExists(command) {
6
+ const isWin = process.platform === "win32";
7
+ const lookup = isWin ? "where" : "which";
8
+ try {
9
+ await execFileAsync(lookup, [command]);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ export function normalizePath(filePath, cwd) {
17
+ const rel = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
18
+ return rel.split(path.sep).join("/");
19
+ }
20
+ export function tryParseJson(raw) {
21
+ const trimmed = raw.trim();
22
+ if (!trimmed)
23
+ return null;
24
+ try {
25
+ return JSON.parse(trimmed);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ export function extractJsonFromOutput(stdout, stderr) {
32
+ const parsed = tryParseJson(stdout);
33
+ if (parsed)
34
+ return parsed;
35
+ const parsedErr = tryParseJson(stderr);
36
+ if (parsedErr)
37
+ return parsedErr;
38
+ const combined = `${stdout}\n${stderr}`.trim();
39
+ if (!combined)
40
+ return null;
41
+ const start = combined.search(/[\[{]/);
42
+ if (start === -1)
43
+ return null;
44
+ const end = Math.max(combined.lastIndexOf("}"), combined.lastIndexOf("]"));
45
+ if (end <= start)
46
+ return null;
47
+ const slice = combined.slice(start, end + 1);
48
+ return tryParseJson(slice);
49
+ }
50
+ export function unique(items) {
51
+ return Array.from(new Set(items));
52
+ }