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 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
+ ```
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require('../src/cli');
4
+
5
+ run().catch((err) => {
6
+ // eslint-disable-next-line no-console
7
+ console.error(err && err.stack ? err.stack : err);
8
+ process.exit(1);
9
+ });
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 };
@@ -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