Skip to content

Commit f35cd96

Browse files
author
Admin
committed
Security: add husky + secretlint pre-commit hook and parser fuzz/security tests
1 parent ccd1335 commit f35cd96

4 files changed

Lines changed: 64 additions & 2 deletions

File tree

.husky/pre-commit

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env sh
2+
. "$(dirname -- "$0")/_/husky.sh"
3+
4+
echo "🔍 [Security] Running pre-commit secret scanning..."
5+
npm run secretlint || {
6+
echo "⛔ Secretlint detected possible secrets. Commit aborted." >&2
7+
exit 1
8+
}
9+

.secretlintrc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rules": [
3+
{
4+
"id": "@secretlint/secretlint-rule-preset-recommend"
5+
}
6+
]
7+
}

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"test": "mocha -r ts-node/register tests/**/*.test.ts --reporter spec"
6+
"test": "mocha -r ts-node/register tests/**/*.test.ts --reporter spec",
7+
"prepare": "husky install",
8+
"secretlint": "secretlint \"**/*\""
79
},
810
"devDependencies": {
911
"@types/js-yaml": "^4.0.9",
1012
"@types/mocha": "^10.0.1",
1113
"@types/node": "^20.0.0",
1214
"mocha": "^10.2.0",
1315
"ts-node": "^10.9.1",
14-
"typescript": "^5.0.0"
16+
"typescript": "^5.0.0",
17+
"husky": "^9.0.11",
18+
"secretlint": "^8.2.4",
19+
"@secretlint/secretlint-rule-preset-recommend": "^8.2.4"
1520
},
1621
"dependencies": {
1722
"js-yaml": "^4.2.0"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { strict as assert } from 'assert';
2+
import { parsePlanReviewOptions } from '../src/parsePlanReview';
3+
4+
describe('Parser security & fuzz tests', () => {
5+
it('handles very large input gracefully (<=50KB) and returns fallback when too large)', () => {
6+
const hugeInput = 'A'.repeat(60 * 1024); // 60KB
7+
const out = parsePlanReviewOptions(hugeInput, undefined, { enableFallback: true });
8+
// Should not throw and should return minimal fallback (2 items)
9+
assert.ok(Array.isArray(out));
10+
assert.ok(out.length <= 50);
11+
});
12+
13+
it('rejects/neutralizes dangerous YAML tags (no functions)', () => {
14+
const maliciousYaml = "target: !!js/function \"function() { require('child_process').execSync('id'); }\"\n";
15+
const out = parsePlanReviewOptions(maliciousYaml, undefined, { enableFallback: true });
16+
// The parser should not return executable JS values
17+
assert.ok(!out.some(item => typeof (item as any).target === 'function'));
18+
});
19+
20+
it('handles deeply nested JSON without crashing (returns fallback or sanitizes)', () => {
21+
let deep: any = {};
22+
let cur = deep;
23+
for (let i = 0; i < 1000; i++) { cur.n = {}; cur = cur.n; }
24+
const payload = JSON.stringify(deep);
25+
const out = parsePlanReviewOptions(payload, undefined, { enableFallback: true });
26+
assert.ok(Array.isArray(out));
27+
});
28+
29+
it('strips control characters from labels', () => {
30+
const payload = JSON.stringify({ id: '1', label: "\u001b[31mBad\u001b[0m" });
31+
const out = parsePlanReviewOptions(payload, undefined, { enableFallback: true });
32+
assert.ok(!out[0].label.includes('\u001b[31m'));
33+
});
34+
35+
it('caps menu items at MAX_MENU_ITEMS (50)', () => {
36+
const items = Array.from({ length: 100 }, (_, i) => ({ id: `i${i}`, label: `Item ${i}` }));
37+
const payload = JSON.stringify(items);
38+
const out = parsePlanReviewOptions(payload, undefined, { enableFallback: true });
39+
assert.ok(out.length <= 50);
40+
});
41+
});

0 commit comments

Comments
 (0)