sec-gate 0.1.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 -0
- package/bin/sec-gate.js +9 -0
- package/package.json +45 -0
- package/scripts/postinstall.js +198 -0
- package/src/cli.js +45 -0
- package/src/commands/install.js +97 -0
- package/src/commands/scan.js +86 -0
- package/src/git/repo.js +11 -0
- package/src/git/stagedFiles.js +24 -0
- package/src/git/trackedFiles.js +15 -0
- package/src/scanners/govulncheck.js +77 -0
- package/src/scanners/osv.js +75 -0
- package/src/scanners/semgrep.js +62 -0
- package/src/suppressions/inlineTag.js +78 -0
- package/vendor-bin/.gitkeep +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# sec-gate
|
|
2
|
+
|
|
3
|
+
A pre-commit security gate that enforces **OWASP Top 10 (2021)** checks before every `git commit`.
|
|
4
|
+
|
|
5
|
+
Covers:
|
|
6
|
+
- **SAST** — static analysis of JS/TS/Go/React code via Semgrep (OWASP Top 10 rules + Express misconfig rules)
|
|
7
|
+
- **SCA** — dependency vulnerability scanning via OSV-Scanner (pnpm) and govulncheck (Go)
|
|
8
|
+
- **Misconfig** — CORS, headers, auth bypass patterns
|
|
9
|
+
|
|
10
|
+
Supports **inline suppression** so developers can acknowledge known false positives with an explicit reason.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install — one command, everything is set up automatically
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g sec-gate
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. This single command:
|
|
21
|
+
|
|
22
|
+
1. Installs the `sec-gate` CLI globally
|
|
23
|
+
2. Downloads the **osv-scanner** binary for your OS automatically
|
|
24
|
+
3. Installs **govulncheck** via `go install` (if Go is available on your machine)
|
|
25
|
+
4. **Installs the pre-commit hook** in your current git repo automatically
|
|
26
|
+
|
|
27
|
+
No extra steps. No separate tool installs. Your next `git commit` is already security-checked.
|
|
28
|
+
|
|
29
|
+
> **Note:** If you run `npm install -g sec-gate` from outside a git repo (e.g. your home directory), run `sec-gate install` once inside the repo afterwards.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What happens on every `git commit`
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
git commit
|
|
37
|
+
↓
|
|
38
|
+
pre-commit hook fires automatically
|
|
39
|
+
↓
|
|
40
|
+
sec-gate scan --staged
|
|
41
|
+
↓
|
|
42
|
+
┌─────────────────────────────────────────────────────┐
|
|
43
|
+
│ SAST — Semgrep scans staged .js/.ts/.go files │
|
|
44
|
+
│ against OWASP Top 10 + Express rules │
|
|
45
|
+
├─────────────────────────────────────────────────────┤
|
|
46
|
+
│ SCA — OSV-Scanner checks pnpm-lock.yaml │
|
|
47
|
+
│ govulncheck checks go.mod │
|
|
48
|
+
│ (only when those files are staged) │
|
|
49
|
+
└─────────────────────────────────────────────────────┘
|
|
50
|
+
↓
|
|
51
|
+
Inline suppression tags filtered out
|
|
52
|
+
↓
|
|
53
|
+
Any findings? → commit BLOCKED, findings printed
|
|
54
|
+
No findings? → commit proceeds
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
sec-gate --help
|
|
63
|
+
|
|
64
|
+
install Installs the pre-commit hook in the current git repo
|
|
65
|
+
scan Runs SAST/SCA checks
|
|
66
|
+
--staged scan only staged files (used by pre-commit hook)
|
|
67
|
+
(no flag) scan all tracked files
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Inline suppression
|
|
73
|
+
|
|
74
|
+
If a finding is a known false positive, add a comment **near the flagged line**:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// security-scan: disable rule-id: javascript.express.security.cors-misconfiguration.cors-misconfiguration reason: internal-only API, safe
|
|
78
|
+
app.use(cors({ origin: '*' }));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```go
|
|
82
|
+
// security-scan: disable rule-id: go.lang.security.audit.dangerous-exec-command.dangerous-exec-command reason: input validated upstream
|
|
83
|
+
exec.Command(cmd)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use `rule-id: *` to suppress **all** findings near that line:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
// security-scan: disable rule-id: * reason: test fixture only
|
|
90
|
+
doSomethingDangerous();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Bypass (emergency only)
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
SEC_GATE_SKIP=1 git commit -m "emergency fix"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Auto-setup for the whole team (optional but recommended)
|
|
104
|
+
|
|
105
|
+
Add this to your **project's** `package.json` so every developer gets the hook automatically when they run `npm install`:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
"scripts": {
|
|
109
|
+
"prepare": "sec-gate install"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Then the workflow for any new developer joining the team is:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm install -g sec-gate # global tool install (once per machine)
|
|
117
|
+
npm install # prepare script auto-installs the hook
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## GitHub Actions — CI gate + PR comments
|
|
123
|
+
|
|
124
|
+
Copy `.github/workflows/security-gate.yml` from this repo into your project to get:
|
|
125
|
+
- Full scan on every pull request
|
|
126
|
+
- Automatic PR comment with findings output
|
|
127
|
+
- PR check blocked if any findings remain
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## OWASP Top 10 (2021) coverage
|
|
132
|
+
|
|
133
|
+
| # | Category | How covered |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| A01 | Broken Access Control | Semgrep `owasp-top10` ruleset |
|
|
136
|
+
| A02 | Cryptographic Failures | Semgrep `owasp-top10` ruleset |
|
|
137
|
+
| A03 | Injection | Semgrep `owasp-top10` ruleset |
|
|
138
|
+
| A04 | Insecure Design | Semgrep `owasp-top10` ruleset |
|
|
139
|
+
| A05 | Security Misconfiguration | Semgrep `owasp-top10` + Express rules |
|
|
140
|
+
| A06 | Vulnerable Components | OSV-Scanner (pnpm) + govulncheck (Go) |
|
|
141
|
+
| A07 | Authentication Failures | Semgrep `owasp-top10` ruleset |
|
|
142
|
+
| A08 | Software Integrity Failures | Semgrep `owasp-top10` ruleset |
|
|
143
|
+
| A09 | Logging Failures | Semgrep `owasp-top10` ruleset |
|
|
144
|
+
| A10 | Server-Side Request Forgery | Semgrep `owasp-top10` ruleset |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Go SCA note
|
|
149
|
+
|
|
150
|
+
`govulncheck` requires Go to be installed on the developer's machine. If Go is not present, Go SCA is skipped with a warning — the install never fails. To enable it:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Install Go: https://go.dev/dl/
|
|
154
|
+
# Then re-run:
|
|
155
|
+
npm install -g sec-gate
|
|
156
|
+
```
|
package/bin/sec-gate.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sec-gate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pre-commit security gate for OWASP Top 10 2021 — SAST, SCA and misconfig checks for Node/Express, Go and React codebases",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sec-gate": "bin/sec-gate.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"postinstall": "node scripts/postinstall.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@pensar/semgrep-node": "^1.2.4"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"security",
|
|
21
|
+
"owasp",
|
|
22
|
+
"sast",
|
|
23
|
+
"sca",
|
|
24
|
+
"pre-commit",
|
|
25
|
+
"semgrep",
|
|
26
|
+
"osv-scanner",
|
|
27
|
+
"govulncheck",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/SUNDRAMBHARDWAJ/sec-gate.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/SUNDRAMBHARDWAJ/sec-gate#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/SUNDRAMBHARDWAJ/sec-gate/issues"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin/",
|
|
40
|
+
"src/",
|
|
41
|
+
"scripts/",
|
|
42
|
+
"vendor-bin/.gitkeep",
|
|
43
|
+
"README.md"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs automatically after `npm install -g sec-gate`.
|
|
5
|
+
* Does three things:
|
|
6
|
+
* 1. Downloads osv-scanner binary for this platform
|
|
7
|
+
* 2. Installs govulncheck via `go install` (if Go is available)
|
|
8
|
+
* 3. Auto-installs the pre-commit hook in the current directory
|
|
9
|
+
* if it is a git repo — so developers never need to run `sec-gate install` manually
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const https = require('https');
|
|
15
|
+
const { execSync, execFileSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const BIN_DIR = path.join(__dirname, '..', 'vendor-bin');
|
|
18
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
19
|
+
|
|
20
|
+
const platform = process.platform; // darwin, linux, win32
|
|
21
|
+
const arch = process.arch; // x64, arm64
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────
|
|
24
|
+
// 1. OSV-Scanner binary download
|
|
25
|
+
// ─────────────────────────────────────────────────────────
|
|
26
|
+
const OSV_VERSION = 'v2.3.5';
|
|
27
|
+
|
|
28
|
+
function osvDownloadUrl() {
|
|
29
|
+
const os = platform === 'darwin' ? 'darwin' : platform === 'win32' ? 'windows' : 'linux';
|
|
30
|
+
const a = arch === 'arm64' ? 'arm64' : 'amd64';
|
|
31
|
+
const ext = platform === 'win32' ? '.exe' : '';
|
|
32
|
+
return `https://github.com/google/osv-scanner/releases/download/${OSV_VERSION}/osv-scanner_${os}_${a}${ext}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function downloadFile(url, dest) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const file = fs.createWriteStream(dest);
|
|
38
|
+
|
|
39
|
+
function get(u) {
|
|
40
|
+
https.get(u, (res) => {
|
|
41
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
42
|
+
return get(res.headers.location);
|
|
43
|
+
}
|
|
44
|
+
if (res.statusCode !== 200) {
|
|
45
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${u}`));
|
|
46
|
+
}
|
|
47
|
+
res.pipe(file);
|
|
48
|
+
file.on('finish', () => file.close(resolve));
|
|
49
|
+
file.on('error', reject);
|
|
50
|
+
}).on('error', reject);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get(url);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function installOsvScanner() {
|
|
58
|
+
const ext = platform === 'win32' ? '.exe' : '';
|
|
59
|
+
const dest = path.join(BIN_DIR, `osv-scanner${ext}`);
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync(dest)) {
|
|
62
|
+
console.log('sec-gate [1/3]: osv-scanner already present');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const url = osvDownloadUrl();
|
|
67
|
+
console.log(`sec-gate [1/3]: downloading osv-scanner...`);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await downloadFile(url, dest);
|
|
71
|
+
fs.chmodSync(dest, 0o755);
|
|
72
|
+
console.log('sec-gate [1/3]: osv-scanner ready');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn(`sec-gate [1/3]: WARNING — osv-scanner download failed: ${err.message}`);
|
|
75
|
+
console.warn(' Node/pnpm SCA will be skipped until this is resolved.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────────────────────────────────
|
|
80
|
+
// 2. govulncheck via `go install`
|
|
81
|
+
// ─────────────────────────────────────────────────────────
|
|
82
|
+
function installGovulncheck() {
|
|
83
|
+
const ext = platform === 'win32' ? '.exe' : '';
|
|
84
|
+
const dest = path.join(BIN_DIR, `govulncheck${ext}`);
|
|
85
|
+
|
|
86
|
+
if (fs.existsSync(dest)) {
|
|
87
|
+
console.log('sec-gate [2/3]: govulncheck already present');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
execSync('go version', { stdio: 'ignore' });
|
|
93
|
+
} catch {
|
|
94
|
+
console.warn('sec-gate [2/3]: WARNING — Go not found. Go SCA will be skipped.');
|
|
95
|
+
console.warn(' Install Go from https://go.dev/dl/ and re-run: npm i -g sec-gate');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
console.log('sec-gate [2/3]: installing govulncheck...');
|
|
101
|
+
const gopath = execSync('go env GOPATH', { encoding: 'utf8' }).trim();
|
|
102
|
+
execSync('go install golang.org/x/vuln/cmd/govulncheck@latest', { stdio: 'inherit' });
|
|
103
|
+
|
|
104
|
+
const goSrc = path.join(gopath, 'bin', `govulncheck${ext}`);
|
|
105
|
+
if (fs.existsSync(goSrc)) {
|
|
106
|
+
fs.copyFileSync(goSrc, dest);
|
|
107
|
+
fs.chmodSync(dest, 0o755);
|
|
108
|
+
console.log('sec-gate [2/3]: govulncheck ready');
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.warn(`sec-gate [2/3]: WARNING — govulncheck install failed: ${err.message}`);
|
|
112
|
+
console.warn(' Go SCA will be skipped.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────
|
|
117
|
+
// 3. Auto-install pre-commit hook in the current git repo
|
|
118
|
+
// ─────────────────────────────────────────────────────────
|
|
119
|
+
const HOOK_MARKER = '# installed-by: sec-gate';
|
|
120
|
+
|
|
121
|
+
function buildHookScript() {
|
|
122
|
+
return [
|
|
123
|
+
'#!/usr/bin/env sh',
|
|
124
|
+
HOOK_MARKER,
|
|
125
|
+
'',
|
|
126
|
+
'# Set SEC_GATE_SKIP=1 to bypass (emergency only)',
|
|
127
|
+
'if [ "$SEC_GATE_SKIP" = "1" ]; then',
|
|
128
|
+
' echo "sec-gate: skipped (SEC_GATE_SKIP=1)"',
|
|
129
|
+
' exit 0',
|
|
130
|
+
'fi',
|
|
131
|
+
'',
|
|
132
|
+
'ROOT_DIR=$(git rev-parse --show-toplevel) || exit 1',
|
|
133
|
+
'cd "$ROOT_DIR" || exit 1',
|
|
134
|
+
'',
|
|
135
|
+
'if command -v sec-gate >/dev/null 2>&1; then',
|
|
136
|
+
' sec-gate scan --staged',
|
|
137
|
+
' exit $?',
|
|
138
|
+
'else',
|
|
139
|
+
' echo "sec-gate: not found in PATH. Run: npm install -g sec-gate"',
|
|
140
|
+
' exit 1',
|
|
141
|
+
'fi',
|
|
142
|
+
''
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function autoInstallHook() {
|
|
147
|
+
let repoRoot;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
repoRoot = execSync('git rev-parse --show-toplevel', {
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
153
|
+
}).trim();
|
|
154
|
+
} catch {
|
|
155
|
+
// Not inside a git repo — skip silently (e.g., CI machines, temp dirs)
|
|
156
|
+
console.log('sec-gate [3/3]: not inside a git repo, skipping hook install');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const hookDir = path.join(repoRoot, '.git', 'hooks');
|
|
161
|
+
const hookPath = path.join(hookDir, 'pre-commit');
|
|
162
|
+
|
|
163
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
164
|
+
|
|
165
|
+
// Already installed by us — nothing to do
|
|
166
|
+
if (fs.existsSync(hookPath)) {
|
|
167
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
168
|
+
if (existing.includes(HOOK_MARKER)) {
|
|
169
|
+
console.log('sec-gate [3/3]: pre-commit hook already installed');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Back up a hook that belongs to something else
|
|
174
|
+
const backup = `${hookPath}.sec-gate.bak`;
|
|
175
|
+
fs.copyFileSync(hookPath, backup);
|
|
176
|
+
console.log(`sec-gate [3/3]: backed up existing hook → ${backup}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(hookPath, buildHookScript(), { encoding: 'utf8', mode: 0o755 });
|
|
180
|
+
console.log(`sec-gate [3/3]: pre-commit hook installed in ${repoRoot}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────
|
|
184
|
+
// Main
|
|
185
|
+
// ─────────────────────────────────────────────────────────
|
|
186
|
+
async function main() {
|
|
187
|
+
console.log('\nsec-gate: setting up...\n');
|
|
188
|
+
await installOsvScanner();
|
|
189
|
+
installGovulncheck();
|
|
190
|
+
autoInstallHook();
|
|
191
|
+
console.log('\nsec-gate: ready. Your commits are now security-checked.\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
main().catch((err) => {
|
|
195
|
+
// Never fail the install — degraded mode is always better than a blocked install.
|
|
196
|
+
console.warn('sec-gate postinstall warning:', err.message);
|
|
197
|
+
process.exit(0);
|
|
198
|
+
});
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function usage() {
|
|
4
|
+
// eslint-disable-next-line no-console
|
|
5
|
+
console.log(`sec-gate - OWASP Top 10 security gate\n\nUsage:\n sec-gate install\n sec-gate scan --staged\n\nCommands:\n install Installs the local git pre-commit hook for this repo\n scan Runs SAST/SCA checks (supports --staged)\n`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const args = { _: [] };
|
|
10
|
+
for (const a of argv) {
|
|
11
|
+
if (a === '--staged') args.staged = true;
|
|
12
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
13
|
+
else args._.push(a);
|
|
14
|
+
}
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function run() {
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const args = parseArgs(argv);
|
|
21
|
+
|
|
22
|
+
if (args.help || args._.length === 0) {
|
|
23
|
+
usage();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cmd = args._[0];
|
|
28
|
+
|
|
29
|
+
if (cmd === 'install') {
|
|
30
|
+
const { installHook } = require('./commands/install');
|
|
31
|
+
await installHook();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (cmd === 'scan') {
|
|
36
|
+
const { scan } = require('./commands/scan');
|
|
37
|
+
await scan({ staged: !!args.staged });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
usage();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { run };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getRepoRoot } = require('../git/repo');
|
|
4
|
+
|
|
5
|
+
const HOOK_MARKER = '# installed-by: sec-gate';
|
|
6
|
+
|
|
7
|
+
function getHookPath(repoRoot) {
|
|
8
|
+
return path.join(repoRoot, '.git', 'hooks', 'pre-commit');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildHookScript() {
|
|
12
|
+
return [
|
|
13
|
+
'#!/usr/bin/env sh',
|
|
14
|
+
HOOK_MARKER,
|
|
15
|
+
'',
|
|
16
|
+
'# Set SEC_GATE_SKIP=1 to bypass (emergency only)',
|
|
17
|
+
'if [ "$SEC_GATE_SKIP" = "1" ]; then',
|
|
18
|
+
' echo "sec-gate: skipped (SEC_GATE_SKIP=1)"',
|
|
19
|
+
' exit 0',
|
|
20
|
+
'fi',
|
|
21
|
+
'',
|
|
22
|
+
'ROOT_DIR=$(git rev-parse --show-toplevel) || exit 1',
|
|
23
|
+
'cd "$ROOT_DIR" || exit 1',
|
|
24
|
+
'',
|
|
25
|
+
'if command -v sec-gate >/dev/null 2>&1; then',
|
|
26
|
+
' sec-gate scan --staged',
|
|
27
|
+
' exit $?',
|
|
28
|
+
'else',
|
|
29
|
+
' echo "sec-gate: not found in PATH. Install it: npm install -g sec-gate"',
|
|
30
|
+
' exit 1',
|
|
31
|
+
'fi',
|
|
32
|
+
''
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isAlreadyInstalled(hookPath) {
|
|
37
|
+
if (!fs.existsSync(hookPath)) return false;
|
|
38
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
39
|
+
return content.includes(HOOK_MARKER);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function suggestPrepareScript(repoRoot) {
|
|
43
|
+
const pkgPath = path.join(repoRoot, 'package.json');
|
|
44
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
45
|
+
|
|
46
|
+
let pkg;
|
|
47
|
+
try {
|
|
48
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hasPrepare = pkg.scripts && pkg.scripts.prepare && pkg.scripts.prepare.includes('sec-gate install');
|
|
54
|
+
|
|
55
|
+
if (!hasPrepare) {
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(' TIP: To auto-install this hook for every developer on your team,');
|
|
58
|
+
console.log(' add this to your repo\'s package.json "scripts":');
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(' "prepare": "sec-gate install"');
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(' Then any developer who runs `npm install` in this repo');
|
|
63
|
+
console.log(' gets the pre-commit hook automatically — no manual step needed.');
|
|
64
|
+
console.log('');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function installHook() {
|
|
69
|
+
const repoRoot = getRepoRoot();
|
|
70
|
+
const hookPath = getHookPath(repoRoot);
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
73
|
+
throw new Error('sec-gate install: .git directory not found. Run this inside a git repository.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
77
|
+
|
|
78
|
+
if (isAlreadyInstalled(hookPath)) {
|
|
79
|
+
console.log('sec-gate: pre-commit hook is already installed.');
|
|
80
|
+
suggestPrepareScript(repoRoot);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Backup any existing hook that wasn't installed by us
|
|
85
|
+
if (fs.existsSync(hookPath)) {
|
|
86
|
+
const backupPath = `${hookPath}.sec-gate.bak`;
|
|
87
|
+
fs.copyFileSync(hookPath, backupPath);
|
|
88
|
+
console.log(`sec-gate: backed up existing hook to ${backupPath}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(hookPath, buildHookScript(), { encoding: 'utf8', mode: 0o755 });
|
|
92
|
+
console.log(`sec-gate: pre-commit hook installed at ${hookPath}`);
|
|
93
|
+
|
|
94
|
+
suggestPrepareScript(repoRoot);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { installHook };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { getStagedFiles, hasStagedDependencyFiles } = require('../git/stagedFiles');
|
|
2
|
+
const { listTrackedFiles } = require('../git/trackedFiles');
|
|
3
|
+
const { runSemgrep } = require('../scanners/semgrep');
|
|
4
|
+
const { runOsvScanner } = require('../scanners/osv');
|
|
5
|
+
const { runGovulncheck } = require('../scanners/govulncheck');
|
|
6
|
+
const { applyInlineSuppressions } = require('../suppressions/inlineTag');
|
|
7
|
+
|
|
8
|
+
function formatFinding(f) {
|
|
9
|
+
const loc = f.line ? `${f.path}:${f.line}` : f.path;
|
|
10
|
+
return `- ${loc} [${f.checkId}] ${f.message}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isSemgrepTargetPath(p) {
|
|
14
|
+
if (!p) return false;
|
|
15
|
+
if (p.endsWith('pnpm-lock.yaml')) return false;
|
|
16
|
+
if (p === 'go.mod' || p === 'go.sum') return false;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
p.endsWith('.js') ||
|
|
20
|
+
p.endsWith('.jsx') ||
|
|
21
|
+
p.endsWith('.mjs') ||
|
|
22
|
+
p.endsWith('.cjs') ||
|
|
23
|
+
p.endsWith('.ts') ||
|
|
24
|
+
p.endsWith('.tsx') ||
|
|
25
|
+
p.endsWith('.go')
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function scan({ staged }) {
|
|
30
|
+
const files = staged ? getStagedFiles() : listTrackedFiles();
|
|
31
|
+
const depChanged = staged ? hasStagedDependencyFiles(files) : true;
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log(`sec-gate: scan started (${staged ? 'staged files' : 'tracked files'})`);
|
|
35
|
+
|
|
36
|
+
const allFindings = [];
|
|
37
|
+
|
|
38
|
+
const semgrepTargets = (files || []).filter(isSemgrepTargetPath);
|
|
39
|
+
|
|
40
|
+
// SAST / misconfig
|
|
41
|
+
if (semgrepTargets.length > 0) {
|
|
42
|
+
const sast = await runSemgrep({ files: semgrepTargets });
|
|
43
|
+
allFindings.push(...sast);
|
|
44
|
+
} else {
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.log('sec-gate: no relevant staged/tracked source files for Semgrep; skipping SAST');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// SCA (only when dependency lockfiles or go module files are staged)
|
|
50
|
+
if (staged && !depChanged) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log('sec-gate: dependency files not staged; skipping SCA');
|
|
53
|
+
} else {
|
|
54
|
+
const fs = require('fs');
|
|
55
|
+
|
|
56
|
+
if (fs.existsSync('pnpm-lock.yaml')) {
|
|
57
|
+
const scaOsv = await runOsvScanner({ lockfile: 'pnpm-lock.yaml' });
|
|
58
|
+
allFindings.push(...scaOsv);
|
|
59
|
+
} else {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.log('sec-gate: pnpm-lock.yaml not found; skipping OSV-Scanner');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync('go.mod')) {
|
|
65
|
+
const scaGo = await runGovulncheck({ pattern: './...' });
|
|
66
|
+
allFindings.push(...scaGo);
|
|
67
|
+
} else {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.log('sec-gate: go.mod not found; skipping govulncheck');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const filtered = applyInlineSuppressions({ findings: allFindings });
|
|
74
|
+
|
|
75
|
+
if (filtered.length > 0) {
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.log('\nsec-gate: SECURITY FINDINGS (commit blocked):');
|
|
78
|
+
for (const f of filtered) console.log(formatFinding(f));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.log('sec-gate: no findings after inline suppression');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { scan };
|
package/src/git/repo.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function getRepoRoot() {
|
|
4
|
+
try {
|
|
5
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
6
|
+
} catch {
|
|
7
|
+
throw new Error('sec-gate: run this command inside a git repository');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { getRepoRoot };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function getStagedFiles() {
|
|
4
|
+
// Includes added/copied/modified/renamed/typed.
|
|
5
|
+
const out = execSync('git diff --cached --name-only --diff-filter=ACMRTUXB', {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const files = out
|
|
11
|
+
.split(/\r?\n/)
|
|
12
|
+
.map((s) => s.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
|
|
15
|
+
return files;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasStagedDependencyFiles(files) {
|
|
19
|
+
if (!files || files.length === 0) return false;
|
|
20
|
+
const depNames = new Set(['pnpm-lock.yaml', 'go.mod', 'go.sum']);
|
|
21
|
+
return files.some((f) => depNames.has(f));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { getStagedFiles, hasStagedDependencyFiles };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function listTrackedFiles() {
|
|
4
|
+
const out = execSync('git ls-files', {
|
|
5
|
+
encoding: 'utf8',
|
|
6
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return out
|
|
10
|
+
.split(/\r?\n/)
|
|
11
|
+
.map((s) => s.trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { listTrackedFiles };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Path to the binary installed by postinstall via `go install`
|
|
6
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
7
|
+
const VENDOR_BIN = path.join(__dirname, '..', '..', 'vendor-bin', `govulncheck${ext}`);
|
|
8
|
+
|
|
9
|
+
function getGovulncheckBinary() {
|
|
10
|
+
if (fs.existsSync(VENDOR_BIN)) return VENDOR_BIN;
|
|
11
|
+
|
|
12
|
+
// Fallback: check PATH
|
|
13
|
+
try {
|
|
14
|
+
const found = execFileSync('which', ['govulncheck'], {
|
|
15
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
16
|
+
}).toString().trim();
|
|
17
|
+
if (found) return 'govulncheck';
|
|
18
|
+
} catch {}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseGovulncheckOutput(stdout) {
|
|
24
|
+
const trimmed = (stdout || '').trim();
|
|
25
|
+
if (!trimmed) return [];
|
|
26
|
+
|
|
27
|
+
// govulncheck streams newline-delimited JSON objects.
|
|
28
|
+
const findings = [];
|
|
29
|
+
|
|
30
|
+
const lines = trimmed.split(/\n+/).map((l) => l.trim()).filter(Boolean);
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
let obj;
|
|
34
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
35
|
+
|
|
36
|
+
// Each message has a `finding` key when a vulnerability is detected.
|
|
37
|
+
if (obj && obj.finding) {
|
|
38
|
+
const f = obj.finding;
|
|
39
|
+
findings.push({
|
|
40
|
+
checkId: `GOVULN:${f.osv || f.trace && f.trace[0] && f.trace[0].function || 'unknown'}`,
|
|
41
|
+
path: (f.trace && f.trace[0] && f.trace[0].position && f.trace[0].position.filename) || 'go.mod',
|
|
42
|
+
line: (f.trace && f.trace[0] && f.trace[0].position && f.trace[0].position.line) || undefined,
|
|
43
|
+
message: `${f.osv || 'vulnerability'}: ${f.trace && f.trace[0] && f.trace[0].function ? `called via ${f.trace[0].function}` : 'vulnerable module in use'}`,
|
|
44
|
+
severity: undefined,
|
|
45
|
+
raw: f
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return findings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runGovulncheck({ pattern }) {
|
|
54
|
+
const bin = getGovulncheckBinary();
|
|
55
|
+
|
|
56
|
+
if (!bin) {
|
|
57
|
+
console.warn(
|
|
58
|
+
'sec-gate: govulncheck not found. Go dependency SCA will be skipped.\n' +
|
|
59
|
+
' Install Go and run: go install golang.org/x/vuln/cmd/govulncheck@latest'
|
|
60
|
+
);
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const stdout = execFileSync(bin, ['-json', pattern], {
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
68
|
+
});
|
|
69
|
+
return parseGovulncheckOutput(stdout);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// exit code 3 = vulnerabilities found — parse stdout anyway
|
|
72
|
+
const stdout = e && e.stdout ? e.stdout.toString('utf8') : '';
|
|
73
|
+
return parseGovulncheckOutput(stdout);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { runGovulncheck };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
// Path to the binary downloaded by postinstall
|
|
7
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
8
|
+
const VENDOR_BIN = path.join(__dirname, '..', '..', 'vendor-bin', `osv-scanner${ext}`);
|
|
9
|
+
|
|
10
|
+
function getOsvBinary() {
|
|
11
|
+
if (fs.existsSync(VENDOR_BIN)) return VENDOR_BIN;
|
|
12
|
+
|
|
13
|
+
// Fallback: check PATH (manual installs)
|
|
14
|
+
try {
|
|
15
|
+
const found = execFileSync('which', ['osv-scanner'], {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
17
|
+
}).toString().trim();
|
|
18
|
+
if (found) return 'osv-scanner';
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runOsvScanner({ lockfile }) {
|
|
25
|
+
const bin = getOsvBinary();
|
|
26
|
+
|
|
27
|
+
if (!bin) {
|
|
28
|
+
console.warn(
|
|
29
|
+
'sec-gate: osv-scanner not found. Node/pnpm dependency SCA will be skipped.\n' +
|
|
30
|
+
' Run `npm i -g sec-gate` again, or install manually: https://google.github.io/osv-scanner/installation'
|
|
31
|
+
);
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(lockfile)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const out = path.join(os.tmpdir(), `sec-gate-osv-${Date.now()}.json`);
|
|
40
|
+
|
|
41
|
+
const args = ['scan', '-L', lockfile, '--format', 'json', '--output-file', out];
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
execFileSync(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
45
|
+
} catch {
|
|
46
|
+
// exit code 1 = vulnerabilities found — expected, not an error
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(out)) return [];
|
|
50
|
+
const text = fs.readFileSync(out, 'utf8');
|
|
51
|
+
if (!text) return [];
|
|
52
|
+
|
|
53
|
+
const parsed = JSON.parse(text);
|
|
54
|
+
const results = parsed.results || [];
|
|
55
|
+
|
|
56
|
+
const findings = [];
|
|
57
|
+
for (const result of results) {
|
|
58
|
+
for (const pkg of result.packages || []) {
|
|
59
|
+
for (const vuln of pkg.vulnerabilities || []) {
|
|
60
|
+
findings.push({
|
|
61
|
+
checkId: `OSV:${vuln.id || 'unknown'}`,
|
|
62
|
+
path: lockfile,
|
|
63
|
+
line: undefined,
|
|
64
|
+
message: `${pkg.package && pkg.package.name ? pkg.package.name : 'dependency'}: ${vuln.summary || vuln.id}`,
|
|
65
|
+
severity: undefined,
|
|
66
|
+
raw: vuln
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { runOsvScanner };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
// Language detection for @pensar/semgrep-node
|
|
4
|
+
function getLanguage(filePath) {
|
|
5
|
+
if (filePath.endsWith('.go')) return 'go';
|
|
6
|
+
if (filePath.endsWith('.py')) return 'python';
|
|
7
|
+
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) return 'ts';
|
|
8
|
+
return 'js';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeSemgrepNodeFinding(issue, filePath) {
|
|
12
|
+
return {
|
|
13
|
+
checkId: issue.issueId || issue.rule || 'semgrep',
|
|
14
|
+
path: filePath,
|
|
15
|
+
line: issue.startLineNumber,
|
|
16
|
+
message: issue.message,
|
|
17
|
+
severity: issue.severity,
|
|
18
|
+
owasp: issue.owasp || [],
|
|
19
|
+
raw: issue
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function runSemgrep({ files }) {
|
|
24
|
+
let semgrepScan;
|
|
25
|
+
try {
|
|
26
|
+
semgrepScan = require('@pensar/semgrep-node').default;
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'sec-gate: @pensar/semgrep-node not found. Run `npm i -g sec-gate` again to reinstall.'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const allFindings = [];
|
|
34
|
+
|
|
35
|
+
for (const filePath of files) {
|
|
36
|
+
const lang = getLanguage(filePath);
|
|
37
|
+
const absPath = path.resolve(filePath);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Scan with OWASP Top 10 rules bundled inside @pensar/semgrep-node
|
|
41
|
+
const issues = await semgrepScan(absPath, {
|
|
42
|
+
language: lang,
|
|
43
|
+
ruleSets: ['owasp-top10']
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
for (const issue of issues) {
|
|
47
|
+
allFindings.push(normalizeSemgrepNodeFinding(issue, filePath));
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// If semgrep binary not yet downloaded for this platform, warn but don't crash.
|
|
51
|
+
if (err && err.message && err.message.includes('ENOENT')) {
|
|
52
|
+
console.warn(`sec-gate: semgrep binary not ready for ${lang}; skipping ${filePath}`);
|
|
53
|
+
} else {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return allFindings;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { runSemgrep };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
function parseSuppressionLine(line) {
|
|
4
|
+
// Supported tag (JS/TS/Go):
|
|
5
|
+
// // security-scan: disable rule-id: <CHECK_ID> reason: <text>
|
|
6
|
+
// Use `rule-id: *` to suppress all findings in the local window.
|
|
7
|
+
const re = /security-scan:\s*disable\s+rule-id:\s*([^\s]+)\s+reason:\s*(.+)$/i;
|
|
8
|
+
const m = line.match(re);
|
|
9
|
+
if (!m) return null;
|
|
10
|
+
return { ruleId: m[1].trim(), reason: m[2].trim() };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasInlineSuppressionNearLine({ fileText, findingLine, checkId, window = 5 }) {
|
|
14
|
+
if (!fileText || typeof findingLine !== 'number') return false;
|
|
15
|
+
|
|
16
|
+
const lines = fileText.split(/\r?\n/);
|
|
17
|
+
const start = Math.max(1, findingLine - window);
|
|
18
|
+
const end = Math.min(lines.length, findingLine + window);
|
|
19
|
+
|
|
20
|
+
for (let i = start; i <= end; i++) {
|
|
21
|
+
const s = parseSuppressionLine(lines[i - 1]);
|
|
22
|
+
if (!s) continue;
|
|
23
|
+
if (s.ruleId === '*' || s.ruleId === String(checkId)) return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasInlineSuppressionAnywhere({ fileText, checkId }) {
|
|
30
|
+
if (!fileText) return false;
|
|
31
|
+
|
|
32
|
+
const lines = fileText.split(/\r?\n/);
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const s = parseSuppressionLine(line);
|
|
35
|
+
if (!s) continue;
|
|
36
|
+
if (s.ruleId === '*' || s.ruleId === String(checkId)) return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applyInlineSuppressions({ findings }) {
|
|
43
|
+
const remaining = [];
|
|
44
|
+
|
|
45
|
+
for (const f of findings) {
|
|
46
|
+
if (!f.path) {
|
|
47
|
+
remaining.push(f);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const text = fs.readFileSync(f.path, 'utf8');
|
|
53
|
+
|
|
54
|
+
let suppressed = false;
|
|
55
|
+
|
|
56
|
+
if (typeof f.line === 'number') {
|
|
57
|
+
suppressed = hasInlineSuppressionNearLine({
|
|
58
|
+
fileText: text,
|
|
59
|
+
findingLine: f.line,
|
|
60
|
+
checkId: f.checkId
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
// SCA findings often lack line numbers; allow suppression anywhere in the file.
|
|
64
|
+
suppressed = hasInlineSuppressionAnywhere({ fileText: text, checkId: f.checkId });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (suppressed) continue;
|
|
68
|
+
} catch {
|
|
69
|
+
// If file can't be read, don't suppress.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
remaining.push(f);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return remaining;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { applyInlineSuppressions };
|
|
File without changes
|