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.
- package/README.md +156 -30
- package/assets/grammars/README.md +9 -0
- package/assets/grammars/tree-sitter-c-sharp.wasm +0 -0
- package/assets/grammars/tree-sitter-c.wasm +0 -0
- package/assets/grammars/tree-sitter-cpp.wasm +0 -0
- package/assets/grammars/tree-sitter-go.wasm +0 -0
- package/assets/grammars/tree-sitter-java.wasm +0 -0
- package/assets/grammars/tree-sitter-kotlin.wasm +0 -0
- package/assets/grammars/tree-sitter-php.wasm +0 -0
- package/assets/grammars/tree-sitter-python.wasm +0 -0
- package/assets/grammars/tree-sitter-ruby.wasm +0 -0
- package/assets/grammars/tree-sitter-rust.wasm +0 -0
- package/assets/grammars/tree-sitter-swift.wasm +0 -0
- package/dist/adapters/bandit.js +41 -0
- package/dist/adapters/brakeman.js +41 -0
- package/dist/adapters/gosec.js +49 -0
- package/dist/adapters/languages.js +29 -0
- package/dist/adapters/runner.js +46 -0
- package/dist/adapters/semgrep.js +59 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/utils.js +52 -0
- package/dist/analysis/infraPatterns.js +196 -0
- package/dist/analysis/universalPatterns.js +56 -0
- package/dist/cli.js +15 -1
- package/dist/config.js +2 -1
- package/dist/native/languages.js +211 -0
- package/dist/native/loader.js +61 -0
- package/dist/native/rules.js +14 -0
- package/dist/native/taint.js +225 -0
- package/dist/scan.js +207 -0
- package/package.json +21 -2
- package/rules/taint/c.json +47 -0
- package/rules/taint/cpp.json +47 -0
- package/rules/taint/csharp.json +99 -0
- package/rules/taint/go.json +86 -0
- package/rules/taint/java.json +101 -0
- package/rules/taint/kotlin.json +86 -0
- package/rules/taint/php.json +100 -0
- package/rules/taint/python.json +108 -0
- package/rules/taint/ruby.json +101 -0
- package/rules/taint/rust.json +86 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
-
##
|
|
33
|
+
## Install(recommended)
|
|
34
|
+
Node.js 20+ is required.
|
|
27
35
|
|
|
28
36
|
```bash
|
|
29
|
-
npm
|
|
30
|
-
npm run dev -- scan --dry-run
|
|
37
|
+
npm i opensecurity
|
|
31
38
|
```
|
|
32
39
|
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
./dist/cli.js scan --dry-run
|
|
49
|
+
opensecurity login --mode api_key --provider openai
|
|
38
50
|
```
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
2) Run a scan:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
opensecurity scan
|
|
56
|
+
```
|
|
41
57
|
|
|
42
|
-
|
|
58
|
+
3) Common options:
|
|
43
59
|
|
|
44
60
|
```bash
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|