sec-gate 0.1.0 → 0.1.2
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/package.json +11 -2
- package/scripts/postinstall.js +64 -30
- package/src/cli.js +16 -1
- package/src/commands/doctor.js +103 -0
- package/src/scanners/semgrep.js +17 -4
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sec-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Pre-commit security gate for OWASP Top 10 2021 — SAST, SCA and misconfig checks for Node/Express, Go and React codebases",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Sundram Bhardwaj",
|
|
7
|
+
"email": "bhardwajsundram0@gmail.com",
|
|
8
|
+
"url": "https://github.com/SUNDRAMBHARDWAJ"
|
|
9
|
+
},
|
|
5
10
|
"bin": {
|
|
6
11
|
"sec-gate": "bin/sec-gate.js"
|
|
7
12
|
},
|
|
@@ -19,13 +24,17 @@
|
|
|
19
24
|
"keywords": [
|
|
20
25
|
"security",
|
|
21
26
|
"owasp",
|
|
27
|
+
"owasp-top-10",
|
|
22
28
|
"sast",
|
|
23
29
|
"sca",
|
|
24
30
|
"pre-commit",
|
|
31
|
+
"git-hook",
|
|
25
32
|
"semgrep",
|
|
26
33
|
"osv-scanner",
|
|
27
34
|
"govulncheck",
|
|
28
|
-
"cli"
|
|
35
|
+
"cli",
|
|
36
|
+
"devsecops",
|
|
37
|
+
"supply-chain"
|
|
29
38
|
],
|
|
30
39
|
"repository": {
|
|
31
40
|
"type": "git",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* sec-gate postinstall script
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE: This script runs after `npm install -g sec-gate` to set up bundled
|
|
7
|
+
* scanning tools so developers need only one install command.
|
|
8
|
+
*
|
|
9
|
+
* WHAT IT DOES (transparently):
|
|
10
|
+
* [1/3] Downloads the osv-scanner binary from Google's official GitHub release
|
|
11
|
+
* URL: https://github.com/google/osv-scanner/releases/
|
|
12
|
+
* Only downloads if not already present. No data is sent anywhere.
|
|
13
|
+
*
|
|
14
|
+
* [2/3] Installs govulncheck using `go install` from golang.org/x/vuln
|
|
15
|
+
* Only runs if Go is installed AND SEC_GATE_GO_INSTALL=1 is set,
|
|
16
|
+
* OR if Go is installed and this is the first time running.
|
|
17
|
+
* Skipped silently if Go is not found.
|
|
18
|
+
*
|
|
19
|
+
* [3/3] Installs a git pre-commit hook in the current directory
|
|
20
|
+
* if it is a git repo. Backs up any existing hook first.
|
|
21
|
+
* Skipped silently if not inside a git repo.
|
|
22
|
+
*
|
|
23
|
+
* OPT-OUT: Set SEC_GATE_SKIP_POSTINSTALL=1 to skip this entire script.
|
|
24
|
+
* Example: SEC_GATE_SKIP_POSTINSTALL=1 npm install -g sec-gate
|
|
25
|
+
*
|
|
26
|
+
* SOURCE: https://github.com/SUNDRAMBHARDWAJ/sec-gate
|
|
10
27
|
*/
|
|
11
28
|
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
// Opt-out: allow users/CI systems to skip postinstall entirely
|
|
30
|
+
if (process.env.SEC_GATE_SKIP_POSTINSTALL === '1') {
|
|
31
|
+
console.log('sec-gate: postinstall skipped (SEC_GATE_SKIP_POSTINSTALL=1)');
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
14
37
|
const https = require('https');
|
|
38
|
+
const crypto = require('crypto');
|
|
15
39
|
const { execSync, execFileSync } = require('child_process');
|
|
16
40
|
|
|
17
41
|
const BIN_DIR = path.join(__dirname, '..', 'vendor-bin');
|
|
@@ -21,7 +45,9 @@ const platform = process.platform; // darwin, linux, win32
|
|
|
21
45
|
const arch = process.arch; // x64, arm64
|
|
22
46
|
|
|
23
47
|
// ─────────────────────────────────────────────────────────
|
|
24
|
-
// 1
|
|
48
|
+
// [1/3] OSV-Scanner binary download
|
|
49
|
+
// Source: https://github.com/google/osv-scanner/releases/
|
|
50
|
+
// No data is sent — we only download a binary from GitHub releases.
|
|
25
51
|
// ─────────────────────────────────────────────────────────
|
|
26
52
|
const OSV_VERSION = 'v2.3.5';
|
|
27
53
|
|
|
@@ -59,47 +85,55 @@ async function installOsvScanner() {
|
|
|
59
85
|
const dest = path.join(BIN_DIR, `osv-scanner${ext}`);
|
|
60
86
|
|
|
61
87
|
if (fs.existsSync(dest)) {
|
|
62
|
-
console.log('sec-gate [1/3]: osv-scanner already present');
|
|
88
|
+
console.log('sec-gate [1/3]: osv-scanner already present, skipping download');
|
|
63
89
|
return;
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
const url = osvDownloadUrl();
|
|
67
|
-
console.log(`sec-gate [1/3]: downloading osv-scanner
|
|
93
|
+
console.log(`sec-gate [1/3]: downloading osv-scanner ${OSV_VERSION}`);
|
|
94
|
+
console.log(` source: ${url}`);
|
|
68
95
|
|
|
69
96
|
try {
|
|
70
97
|
await downloadFile(url, dest);
|
|
71
98
|
fs.chmodSync(dest, 0o755);
|
|
72
|
-
|
|
99
|
+
|
|
100
|
+
// Print a SHA256 fingerprint so security-conscious users can verify
|
|
101
|
+
const hash = crypto.createHash('sha256').update(fs.readFileSync(dest)).digest('hex');
|
|
102
|
+
console.log(`sec-gate [1/3]: osv-scanner ready (sha256: ${hash})`);
|
|
103
|
+
console.log(` verify at: https://github.com/google/osv-scanner/releases/tag/${OSV_VERSION}`);
|
|
73
104
|
} catch (err) {
|
|
74
105
|
console.warn(`sec-gate [1/3]: WARNING — osv-scanner download failed: ${err.message}`);
|
|
75
|
-
console.warn(' Node/pnpm SCA will be skipped
|
|
106
|
+
console.warn(' Node/pnpm SCA will be skipped. Re-run: npm i -g sec-gate');
|
|
76
107
|
}
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
// ─────────────────────────────────────────────────────────
|
|
80
|
-
// 2
|
|
111
|
+
// [2/3] govulncheck via `go install`
|
|
112
|
+
// Only installs if Go is available on this machine.
|
|
113
|
+
// Source: https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
|
|
81
114
|
// ─────────────────────────────────────────────────────────
|
|
82
115
|
function installGovulncheck() {
|
|
83
116
|
const ext = platform === 'win32' ? '.exe' : '';
|
|
84
117
|
const dest = path.join(BIN_DIR, `govulncheck${ext}`);
|
|
85
118
|
|
|
86
119
|
if (fs.existsSync(dest)) {
|
|
87
|
-
console.log('sec-gate [2/3]: govulncheck already present');
|
|
120
|
+
console.log('sec-gate [2/3]: govulncheck already present, skipping install');
|
|
88
121
|
return;
|
|
89
122
|
}
|
|
90
123
|
|
|
124
|
+
// Check if Go is available — skip silently if not
|
|
91
125
|
try {
|
|
92
|
-
|
|
126
|
+
execFileSync('go', ['version'], { stdio: 'ignore' });
|
|
93
127
|
} catch {
|
|
94
|
-
console.
|
|
95
|
-
console.
|
|
128
|
+
console.log('sec-gate [2/3]: Go not found — skipping govulncheck install');
|
|
129
|
+
console.log(' To enable Go SCA: install Go (https://go.dev/dl/) and re-run: npm i -g sec-gate');
|
|
96
130
|
return;
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
try {
|
|
100
|
-
console.log('sec-gate [2/3]: installing govulncheck
|
|
101
|
-
const gopath =
|
|
102
|
-
|
|
134
|
+
console.log('sec-gate [2/3]: installing govulncheck via `go install golang.org/x/vuln/cmd/govulncheck@latest`');
|
|
135
|
+
const gopath = execFileSync('go', ['env', 'GOPATH'], { encoding: 'utf8' }).trim();
|
|
136
|
+
execFileSync('go', ['install', 'golang.org/x/vuln/cmd/govulncheck@latest'], { stdio: 'inherit' });
|
|
103
137
|
|
|
104
138
|
const goSrc = path.join(gopath, 'bin', `govulncheck${ext}`);
|
|
105
139
|
if (fs.existsSync(goSrc)) {
|
|
@@ -114,7 +148,9 @@ function installGovulncheck() {
|
|
|
114
148
|
}
|
|
115
149
|
|
|
116
150
|
// ─────────────────────────────────────────────────────────
|
|
117
|
-
// 3
|
|
151
|
+
// [3/3] Auto-install pre-commit hook in the current git repo
|
|
152
|
+
// Backs up any existing hook before writing.
|
|
153
|
+
// Skipped silently if not inside a git repo.
|
|
118
154
|
// ─────────────────────────────────────────────────────────
|
|
119
155
|
const HOOK_MARKER = '# installed-by: sec-gate';
|
|
120
156
|
|
|
@@ -147,13 +183,13 @@ function autoInstallHook() {
|
|
|
147
183
|
let repoRoot;
|
|
148
184
|
|
|
149
185
|
try {
|
|
150
|
-
repoRoot =
|
|
186
|
+
repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
151
187
|
encoding: 'utf8',
|
|
152
188
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
153
189
|
}).trim();
|
|
154
190
|
} catch {
|
|
155
|
-
|
|
156
|
-
console.log('sec-gate
|
|
191
|
+
console.log('sec-gate [3/3]: not inside a git repo — skipping hook install');
|
|
192
|
+
console.log(' Run `sec-gate install` inside your project to install the hook.');
|
|
157
193
|
return;
|
|
158
194
|
}
|
|
159
195
|
|
|
@@ -162,15 +198,12 @@ function autoInstallHook() {
|
|
|
162
198
|
|
|
163
199
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
164
200
|
|
|
165
|
-
// Already installed by us — nothing to do
|
|
166
201
|
if (fs.existsSync(hookPath)) {
|
|
167
202
|
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
168
203
|
if (existing.includes(HOOK_MARKER)) {
|
|
169
204
|
console.log('sec-gate [3/3]: pre-commit hook already installed');
|
|
170
205
|
return;
|
|
171
206
|
}
|
|
172
|
-
|
|
173
|
-
// Back up a hook that belongs to something else
|
|
174
207
|
const backup = `${hookPath}.sec-gate.bak`;
|
|
175
208
|
fs.copyFileSync(hookPath, backup);
|
|
176
209
|
console.log(`sec-gate [3/3]: backed up existing hook → ${backup}`);
|
|
@@ -184,7 +217,8 @@ function autoInstallHook() {
|
|
|
184
217
|
// Main
|
|
185
218
|
// ─────────────────────────────────────────────────────────
|
|
186
219
|
async function main() {
|
|
187
|
-
console.log('\nsec-gate: setting up
|
|
220
|
+
console.log('\nsec-gate: setting up bundled scanners...');
|
|
221
|
+
console.log(' (set SEC_GATE_SKIP_POSTINSTALL=1 to skip this)\n');
|
|
188
222
|
await installOsvScanner();
|
|
189
223
|
installGovulncheck();
|
|
190
224
|
autoInstallHook();
|
|
@@ -192,7 +226,7 @@ async function main() {
|
|
|
192
226
|
}
|
|
193
227
|
|
|
194
228
|
main().catch((err) => {
|
|
195
|
-
// Never fail the install — degraded mode is
|
|
229
|
+
// Never fail the npm install itself — degraded mode is better than blocked install
|
|
196
230
|
console.warn('sec-gate postinstall warning:', err.message);
|
|
197
231
|
process.exit(0);
|
|
198
232
|
});
|
package/src/cli.js
CHANGED
|
@@ -2,7 +2,16 @@ const path = require('path');
|
|
|
2
2
|
|
|
3
3
|
function usage() {
|
|
4
4
|
// eslint-disable-next-line no-console
|
|
5
|
-
console.log(
|
|
5
|
+
console.log([
|
|
6
|
+
'sec-gate - OWASP Top 10 security gate',
|
|
7
|
+
'',
|
|
8
|
+
'Usage:',
|
|
9
|
+
' sec-gate install Install the pre-commit hook in this repo',
|
|
10
|
+
' sec-gate scan Scan all tracked files',
|
|
11
|
+
' sec-gate scan --staged Scan only staged files (used by pre-commit hook)',
|
|
12
|
+
' sec-gate doctor Check all components are installed and working',
|
|
13
|
+
''
|
|
14
|
+
].join('\n'));
|
|
6
15
|
}
|
|
7
16
|
|
|
8
17
|
function parseArgs(argv) {
|
|
@@ -38,6 +47,12 @@ async function run() {
|
|
|
38
47
|
return;
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
if (cmd === 'doctor') {
|
|
51
|
+
const { doctor } = require('./commands/doctor');
|
|
52
|
+
await doctor();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
usage();
|
|
42
57
|
process.exit(1);
|
|
43
58
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function check(label, fn) {
|
|
6
|
+
try {
|
|
7
|
+
const result = fn();
|
|
8
|
+
console.log(` [OK] ${label}${result ? ': ' + result : ''}`);
|
|
9
|
+
return true;
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.log(` [FAIL] ${label}: ${err.message}`);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function doctor() {
|
|
17
|
+
console.log('\nsec-gate doctor — checking all components\n');
|
|
18
|
+
|
|
19
|
+
// 1. sec-gate itself
|
|
20
|
+
check('sec-gate CLI', () => {
|
|
21
|
+
const pkg = require('../../package.json');
|
|
22
|
+
return `v${pkg.version}`;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 2. @pensar/semgrep-node
|
|
26
|
+
console.log('\n--- SAST (Semgrep) ---');
|
|
27
|
+
const semgrepOk = check('@pensar/semgrep-node installed', () => {
|
|
28
|
+
const pkg = require('@pensar/semgrep-node');
|
|
29
|
+
return 'found';
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (semgrepOk) {
|
|
33
|
+
// Try actually loading the binary by doing a tiny scan on a temp file
|
|
34
|
+
await (async () => {
|
|
35
|
+
try {
|
|
36
|
+
const os = require('os');
|
|
37
|
+
const tmpFile = path.join(os.tmpdir(), 'sec-gate-test.js');
|
|
38
|
+
fs.writeFileSync(tmpFile, '// test\nconst x = 1;\n');
|
|
39
|
+
const scan = require('@pensar/semgrep-node').default;
|
|
40
|
+
await scan(tmpFile, { language: 'js', ruleSets: ['owasp-top10'] });
|
|
41
|
+
fs.unlinkSync(tmpFile);
|
|
42
|
+
console.log(' [OK] semgrep binary: working');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.log(` [FAIL] semgrep binary: ${err.message}`);
|
|
45
|
+
console.log(' Fix: the semgrep binary may not be downloaded yet.');
|
|
46
|
+
console.log(' Try running `sec-gate scan` on a JS file once to trigger download.');
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. osv-scanner
|
|
52
|
+
console.log('\n--- SCA: Node/pnpm (OSV-Scanner) ---');
|
|
53
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
54
|
+
const vendorOsv = path.join(__dirname, '..', '..', 'vendor-bin', `osv-scanner${ext}`);
|
|
55
|
+
|
|
56
|
+
check('osv-scanner binary (vendor-bin)', () => {
|
|
57
|
+
if (fs.existsSync(vendorOsv)) return vendorOsv;
|
|
58
|
+
throw new Error('not found in vendor-bin');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
check('osv-scanner executable', () => {
|
|
62
|
+
if (!fs.existsSync(vendorOsv)) throw new Error('binary missing');
|
|
63
|
+
const out = execFileSync(vendorOsv, ['--version'], { encoding: 'utf8' }).trim();
|
|
64
|
+
return out;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 4. govulncheck
|
|
68
|
+
console.log('\n--- SCA: Go (govulncheck) ---');
|
|
69
|
+
const vendorGo = path.join(__dirname, '..', '..', 'vendor-bin', `govulncheck${ext}`);
|
|
70
|
+
|
|
71
|
+
check('govulncheck binary (vendor-bin)', () => {
|
|
72
|
+
if (fs.existsSync(vendorGo)) return vendorGo;
|
|
73
|
+
throw new Error('not found — Go SCA will be skipped (Go may not be installed)');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 5. git hook
|
|
77
|
+
console.log('\n--- Pre-commit hook ---');
|
|
78
|
+
check('git available', () => {
|
|
79
|
+
execFileSync('git', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
80
|
+
return 'found';
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
85
|
+
encoding: 'utf8',
|
|
86
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
87
|
+
}).trim();
|
|
88
|
+
|
|
89
|
+
const hookPath = path.join(repoRoot, '.git', 'hooks', 'pre-commit');
|
|
90
|
+
check('pre-commit hook installed', () => {
|
|
91
|
+
if (!fs.existsSync(hookPath)) throw new Error('not found — run `sec-gate install`');
|
|
92
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
93
|
+
if (!content.includes('installed-by: sec-gate')) throw new Error('hook exists but was not installed by sec-gate');
|
|
94
|
+
return hookPath;
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
console.log(' [SKIP] pre-commit hook: not inside a git repo');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('\nsec-gate doctor done.\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { doctor };
|
package/src/scanners/semgrep.js
CHANGED
|
@@ -34,24 +34,37 @@ async function runSemgrep({ files }) {
|
|
|
34
34
|
|
|
35
35
|
for (const filePath of files) {
|
|
36
36
|
const lang = getLanguage(filePath);
|
|
37
|
+
// security-scan: disable rule-id: path-join-resolve-traversal reason: filePath comes from `git diff --cached --name-only` output, not from user input
|
|
37
38
|
const absPath = path.resolve(filePath);
|
|
38
39
|
|
|
39
40
|
try {
|
|
40
|
-
//
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(`sec-gate: scanning ${filePath} (${lang}) with owasp-top10 rules...`);
|
|
43
|
+
|
|
41
44
|
const issues = await semgrepScan(absPath, {
|
|
42
45
|
language: lang,
|
|
43
46
|
ruleSets: ['owasp-top10']
|
|
44
47
|
});
|
|
45
48
|
|
|
49
|
+
if (issues.length > 0) {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log(`sec-gate: found ${issues.length} finding(s) in ${filePath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
for (const issue of issues) {
|
|
47
55
|
allFindings.push(normalizeSemgrepNodeFinding(issue, filePath));
|
|
48
56
|
}
|
|
49
57
|
} catch (err) {
|
|
50
|
-
// If semgrep binary not yet downloaded for this platform, warn but don't crash.
|
|
51
58
|
if (err && err.message && err.message.includes('ENOENT')) {
|
|
52
|
-
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.warn(`sec-gate: WARNING — semgrep binary not found for language "${lang}"`);
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.warn(' The semgrep binary needs to be downloaded by @pensar/semgrep-node.');
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(' Run `sec-gate doctor` to diagnose, or re-install: npm i -g sec-gate');
|
|
53
65
|
} else {
|
|
54
|
-
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.warn(`sec-gate: WARNING — semgrep scan failed for ${filePath}: ${err.message}`);
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
}
|