@capgo/capgo-sec 1.0.4
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/.github/workflows/bump_version.yml +83 -0
- package/.github/workflows/ci.yml +44 -0
- package/AGENTS.md +125 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/bun.lock +146 -0
- package/dist/cli/index.js +13248 -0
- package/dist/index.js +8273 -0
- package/package.json +53 -0
- package/renovate.json +39 -0
- package/src/cli/index.ts +183 -0
- package/src/index.ts +31 -0
- package/src/rules/android.ts +392 -0
- package/src/rules/authentication.ts +261 -0
- package/src/rules/capacitor.ts +435 -0
- package/src/rules/cryptography.ts +190 -0
- package/src/rules/index.ts +56 -0
- package/src/rules/ios.ts +326 -0
- package/src/rules/logging.ts +218 -0
- package/src/rules/network.ts +310 -0
- package/src/rules/secrets.ts +163 -0
- package/src/rules/storage.ts +241 -0
- package/src/rules/webview.ts +232 -0
- package/src/scanners/engine.ts +233 -0
- package/src/types.ts +96 -0
- package/src/utils/reporter.ts +209 -0
- package/test/rules.test.ts +235 -0
- package/test/scanner.test.ts +292 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { allRules, ruleCount } from '../src/rules/index';
|
|
3
|
+
import { secretsRules } from '../src/rules/secrets';
|
|
4
|
+
import { storageRules } from '../src/rules/storage';
|
|
5
|
+
import { networkRules } from '../src/rules/network';
|
|
6
|
+
import { capacitorRules } from '../src/rules/capacitor';
|
|
7
|
+
import { androidRules } from '../src/rules/android';
|
|
8
|
+
import { iosRules } from '../src/rules/ios';
|
|
9
|
+
import { authenticationRules } from '../src/rules/authentication';
|
|
10
|
+
import { webviewRules } from '../src/rules/webview';
|
|
11
|
+
import { cryptographyRules } from '../src/rules/cryptography';
|
|
12
|
+
import { loggingRules, debugRules } from '../src/rules/logging';
|
|
13
|
+
|
|
14
|
+
describe('Security Rules', () => {
|
|
15
|
+
test('should have correct total rule count', () => {
|
|
16
|
+
expect(ruleCount).toBeGreaterThan(60);
|
|
17
|
+
expect(allRules.length).toBe(ruleCount);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('all rules should have required properties', () => {
|
|
21
|
+
for (const rule of allRules) {
|
|
22
|
+
expect(rule.id).toBeDefined();
|
|
23
|
+
expect(rule.name).toBeDefined();
|
|
24
|
+
expect(rule.description).toBeDefined();
|
|
25
|
+
expect(rule.severity).toBeDefined();
|
|
26
|
+
expect(rule.category).toBeDefined();
|
|
27
|
+
expect(rule.remediation).toBeDefined();
|
|
28
|
+
expect(['critical', 'high', 'medium', 'low', 'info']).toContain(rule.severity);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('all rules should have unique IDs', () => {
|
|
33
|
+
const ids = allRules.map(r => r.id);
|
|
34
|
+
const uniqueIds = new Set(ids);
|
|
35
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('secrets rules should detect hardcoded API keys', () => {
|
|
39
|
+
const rule = secretsRules.find(r => r.id === 'SEC001');
|
|
40
|
+
expect(rule).toBeDefined();
|
|
41
|
+
|
|
42
|
+
const testCode = `const apiKey = "AKIA1234567890ABCDEF";`;
|
|
43
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
44
|
+
|
|
45
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
46
|
+
expect(findings[0].severity).toBe('critical');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('secrets rules should detect Firebase keys', () => {
|
|
50
|
+
const rule = secretsRules.find(r => r.id === 'SEC001');
|
|
51
|
+
expect(rule).toBeDefined();
|
|
52
|
+
|
|
53
|
+
const testCode = `const firebase = "AIzaSyDOCAbC123dEf456GhI789jKl012mNo3456";`;
|
|
54
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
55
|
+
|
|
56
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('storage rules should detect sensitive data in Preferences', () => {
|
|
60
|
+
const rule = storageRules.find(r => r.id === 'STO001');
|
|
61
|
+
expect(rule).toBeDefined();
|
|
62
|
+
|
|
63
|
+
const testCode = `Preferences.set({ key: 'userPassword', value: password });`;
|
|
64
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
65
|
+
|
|
66
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
67
|
+
expect(findings[0].severity).toBe('high');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('storage rules should detect localStorage for sensitive data', () => {
|
|
71
|
+
const rule = storageRules.find(r => r.id === 'STO002');
|
|
72
|
+
expect(rule).toBeDefined();
|
|
73
|
+
|
|
74
|
+
const testCode = `localStorage.setItem("authToken", token);`;
|
|
75
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
76
|
+
|
|
77
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('network rules should detect HTTP URLs', () => {
|
|
81
|
+
const rule = networkRules.find(r => r.id === 'NET001');
|
|
82
|
+
expect(rule).toBeDefined();
|
|
83
|
+
|
|
84
|
+
const testCode = `fetch("http://api.example.com/data");`;
|
|
85
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
86
|
+
|
|
87
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
88
|
+
expect(findings[0].severity).toBe('high');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('network rules should allow localhost HTTP', () => {
|
|
92
|
+
const rule = networkRules.find(r => r.id === 'NET001');
|
|
93
|
+
expect(rule).toBeDefined();
|
|
94
|
+
|
|
95
|
+
const testCode = `fetch("http://localhost:3000/api");`;
|
|
96
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
97
|
+
|
|
98
|
+
expect(findings.length).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('capacitor rules should detect WebView debug mode', () => {
|
|
102
|
+
const rule = capacitorRules.find(r => r.id === 'CAP001');
|
|
103
|
+
expect(rule).toBeDefined();
|
|
104
|
+
|
|
105
|
+
const testCode = `
|
|
106
|
+
export default {
|
|
107
|
+
webContentsDebuggingEnabled: true,
|
|
108
|
+
appId: 'com.example.app'
|
|
109
|
+
};
|
|
110
|
+
`;
|
|
111
|
+
const findings = rule!.check!(testCode, 'capacitor.config.ts');
|
|
112
|
+
|
|
113
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
114
|
+
expect(findings[0].severity).toBe('critical');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('capacitor rules should detect eval usage', () => {
|
|
118
|
+
const rule = capacitorRules.find(r => r.id === 'CAP006');
|
|
119
|
+
expect(rule).toBeDefined();
|
|
120
|
+
|
|
121
|
+
const testCode = `const result = eval(userInput);`;
|
|
122
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
123
|
+
|
|
124
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
125
|
+
expect(findings[0].severity).toBe('critical');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('android rules should detect cleartext traffic', () => {
|
|
129
|
+
const rule = androidRules.find(r => r.id === 'AND001');
|
|
130
|
+
expect(rule).toBeDefined();
|
|
131
|
+
|
|
132
|
+
const testXml = `
|
|
133
|
+
<application
|
|
134
|
+
android:usesCleartextTraffic="true"
|
|
135
|
+
android:label="@string/app_name">
|
|
136
|
+
</application>
|
|
137
|
+
`;
|
|
138
|
+
const findings = rule!.check!(testXml, 'AndroidManifest.xml');
|
|
139
|
+
|
|
140
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
141
|
+
expect(findings[0].severity).toBe('critical');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('android rules should detect debug mode', () => {
|
|
145
|
+
const rule = androidRules.find(r => r.id === 'AND002');
|
|
146
|
+
expect(rule).toBeDefined();
|
|
147
|
+
|
|
148
|
+
const testXml = `
|
|
149
|
+
<application
|
|
150
|
+
android:debuggable="true"
|
|
151
|
+
android:label="@string/app_name">
|
|
152
|
+
</application>
|
|
153
|
+
`;
|
|
154
|
+
const findings = rule!.check!(testXml, 'AndroidManifest.xml');
|
|
155
|
+
|
|
156
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('ios rules should detect ATS disabled', () => {
|
|
160
|
+
const rule = iosRules.find(r => r.id === 'IOS001');
|
|
161
|
+
expect(rule).toBeDefined();
|
|
162
|
+
|
|
163
|
+
const testPlist = `
|
|
164
|
+
<key>NSAppTransportSecurity</key>
|
|
165
|
+
<dict>
|
|
166
|
+
<key>NSAllowsArbitraryLoads</key>
|
|
167
|
+
<true />
|
|
168
|
+
</dict>
|
|
169
|
+
`;
|
|
170
|
+
const findings = rule!.check!(testPlist, 'Info.plist');
|
|
171
|
+
|
|
172
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
173
|
+
expect(findings[0].severity).toBe('critical');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('authentication rules should detect weak JWT validation', () => {
|
|
177
|
+
const rule = authenticationRules.find(r => r.id === 'AUTH001');
|
|
178
|
+
expect(rule).toBeDefined();
|
|
179
|
+
|
|
180
|
+
const testCode = `const claims = jwtDecode(token);`;
|
|
181
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
182
|
+
|
|
183
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('authentication rules should detect Math.random in security context', () => {
|
|
187
|
+
const rule = authenticationRules.find(r => r.id === 'AUTH003');
|
|
188
|
+
expect(rule).toBeDefined();
|
|
189
|
+
|
|
190
|
+
const testCode = `const token = Math.random().toString(36);`;
|
|
191
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
192
|
+
|
|
193
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('webview rules should detect innerHTML usage', () => {
|
|
197
|
+
const rule = webviewRules.find(r => r.id === 'WEB001');
|
|
198
|
+
expect(rule).toBeDefined();
|
|
199
|
+
|
|
200
|
+
const testCode = `element.innerHTML = userInput;`;
|
|
201
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
202
|
+
|
|
203
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('cryptography rules should detect weak algorithms', () => {
|
|
207
|
+
const rule = cryptographyRules.find(r => r.id === 'CRY001');
|
|
208
|
+
expect(rule).toBeDefined();
|
|
209
|
+
|
|
210
|
+
const testCode = `const hash = crypto.createHash('md5').update(data).digest('hex');`;
|
|
211
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
212
|
+
|
|
213
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('logging rules should detect sensitive data in logs', () => {
|
|
217
|
+
const rule = loggingRules.find(r => r.id === 'LOG001');
|
|
218
|
+
expect(rule).toBeDefined();
|
|
219
|
+
|
|
220
|
+
const testCode = `console.log('User password:', password);`;
|
|
221
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
222
|
+
|
|
223
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('debug rules should detect debugger statements', () => {
|
|
227
|
+
const rule = debugRules.find(r => r.id === 'DBG001');
|
|
228
|
+
expect(rule).toBeDefined();
|
|
229
|
+
|
|
230
|
+
const testCode = `function test() { debugger; return 1; }`;
|
|
231
|
+
const findings = rule!.check!(testCode, 'test.ts');
|
|
232
|
+
|
|
233
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, expect, test, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import { SecurityScanner } from '../src/scanners/engine';
|
|
3
|
+
import { mkdir, rm } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
const TEST_DIR = '/tmp/capsec-test-project';
|
|
7
|
+
|
|
8
|
+
describe('SecurityScanner', () => {
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
// Create test project directory
|
|
11
|
+
await mkdir(TEST_DIR, { recursive: true });
|
|
12
|
+
await mkdir(join(TEST_DIR, 'src'), { recursive: true });
|
|
13
|
+
await mkdir(join(TEST_DIR, 'android/app/src/main'), { recursive: true });
|
|
14
|
+
await mkdir(join(TEST_DIR, 'ios/App'), { recursive: true });
|
|
15
|
+
|
|
16
|
+
// Create test files with vulnerabilities using Bun.write
|
|
17
|
+
await Bun.write(
|
|
18
|
+
join(TEST_DIR, 'src/api.ts'),
|
|
19
|
+
`
|
|
20
|
+
const API_KEY = "AKIA1234567890ABCDEF";
|
|
21
|
+
const secret = "my_secret_key_for_testing_purposes";
|
|
22
|
+
|
|
23
|
+
export async function fetchData() {
|
|
24
|
+
return fetch("http://api.example.com/data");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function processInput(input: string) {
|
|
28
|
+
return eval(input);
|
|
29
|
+
}
|
|
30
|
+
`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await Bun.write(
|
|
34
|
+
join(TEST_DIR, 'src/auth.ts'),
|
|
35
|
+
`
|
|
36
|
+
import { jwtDecode } from 'jwt-decode';
|
|
37
|
+
|
|
38
|
+
export function getUserFromToken(token: string) {
|
|
39
|
+
const user = jwtDecode(token);
|
|
40
|
+
console.log('User token:', token);
|
|
41
|
+
return user;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function generateSessionId() {
|
|
45
|
+
return Math.random().toString(36);
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await Bun.write(
|
|
51
|
+
join(TEST_DIR, 'capacitor.config.ts'),
|
|
52
|
+
`
|
|
53
|
+
import { CapacitorConfig } from '@capacitor/cli';
|
|
54
|
+
|
|
55
|
+
const config: CapacitorConfig = {
|
|
56
|
+
appId: 'com.example.app',
|
|
57
|
+
appName: 'Test App',
|
|
58
|
+
webDir: 'dist',
|
|
59
|
+
server: {
|
|
60
|
+
cleartext: true
|
|
61
|
+
},
|
|
62
|
+
android: {
|
|
63
|
+
webContentsDebuggingEnabled: true,
|
|
64
|
+
allowMixedContent: true
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default config;
|
|
69
|
+
`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await Bun.write(
|
|
73
|
+
join(TEST_DIR, 'android/app/src/main/AndroidManifest.xml'),
|
|
74
|
+
`
|
|
75
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
76
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
77
|
+
<application
|
|
78
|
+
android:usesCleartextTraffic="true"
|
|
79
|
+
android:debuggable="true"
|
|
80
|
+
android:allowBackup="true">
|
|
81
|
+
</application>
|
|
82
|
+
<uses-permission android:name="android.permission.READ_SMS"/>
|
|
83
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
|
84
|
+
</manifest>
|
|
85
|
+
`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await Bun.write(
|
|
89
|
+
join(TEST_DIR, 'ios/App/Info.plist'),
|
|
90
|
+
`
|
|
91
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
92
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN">
|
|
93
|
+
<plist version="1.0">
|
|
94
|
+
<dict>
|
|
95
|
+
<key>NSAppTransportSecurity</key>
|
|
96
|
+
<dict>
|
|
97
|
+
<key>NSAllowsArbitraryLoads</key>
|
|
98
|
+
<true />
|
|
99
|
+
</dict>
|
|
100
|
+
</dict>
|
|
101
|
+
</plist>
|
|
102
|
+
`
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await Bun.write(
|
|
106
|
+
join(TEST_DIR, 'src/storage.ts'),
|
|
107
|
+
`
|
|
108
|
+
import { Preferences } from '@capacitor/preferences';
|
|
109
|
+
|
|
110
|
+
export async function saveCredentials(password: string) {
|
|
111
|
+
await Preferences.set({ key: 'userPassword', value: password });
|
|
112
|
+
localStorage.setItem('authToken', 'secret');
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterAll(async () => {
|
|
119
|
+
// Cleanup test directory
|
|
120
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should scan project and find vulnerabilities', async () => {
|
|
124
|
+
const scanner = new SecurityScanner({
|
|
125
|
+
path: TEST_DIR
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = await scanner.scan();
|
|
129
|
+
|
|
130
|
+
expect(result.filesScanned).toBeGreaterThan(0);
|
|
131
|
+
expect(result.findings.length).toBeGreaterThan(0);
|
|
132
|
+
expect(result.summary.total).toBeGreaterThan(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('should find critical severity issues', async () => {
|
|
136
|
+
const scanner = new SecurityScanner({
|
|
137
|
+
path: TEST_DIR
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = await scanner.scan();
|
|
141
|
+
|
|
142
|
+
expect(result.summary.critical).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should find high severity issues', async () => {
|
|
146
|
+
const scanner = new SecurityScanner({
|
|
147
|
+
path: TEST_DIR
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await scanner.scan();
|
|
151
|
+
|
|
152
|
+
expect(result.summary.high).toBeGreaterThan(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('should detect secrets in code', async () => {
|
|
156
|
+
const scanner = new SecurityScanner({
|
|
157
|
+
path: TEST_DIR,
|
|
158
|
+
categories: ['secrets']
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = await scanner.scan();
|
|
162
|
+
|
|
163
|
+
const secretFindings = result.findings.filter(f => f.category === 'secrets');
|
|
164
|
+
expect(secretFindings.length).toBeGreaterThan(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should detect network issues', async () => {
|
|
168
|
+
const scanner = new SecurityScanner({
|
|
169
|
+
path: TEST_DIR,
|
|
170
|
+
categories: ['network']
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = await scanner.scan();
|
|
174
|
+
|
|
175
|
+
const networkFindings = result.findings.filter(f => f.category === 'network');
|
|
176
|
+
expect(networkFindings.length).toBeGreaterThan(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should detect capacitor config issues', async () => {
|
|
180
|
+
const scanner = new SecurityScanner({
|
|
181
|
+
path: TEST_DIR,
|
|
182
|
+
categories: ['capacitor']
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const result = await scanner.scan();
|
|
186
|
+
|
|
187
|
+
const capFindings = result.findings.filter(f => f.category === 'capacitor');
|
|
188
|
+
expect(capFindings.length).toBeGreaterThan(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('should detect android issues', async () => {
|
|
192
|
+
const scanner = new SecurityScanner({
|
|
193
|
+
path: TEST_DIR,
|
|
194
|
+
categories: ['android']
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const result = await scanner.scan();
|
|
198
|
+
|
|
199
|
+
const androidFindings = result.findings.filter(f => f.category === 'android');
|
|
200
|
+
expect(androidFindings.length).toBeGreaterThan(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should detect iOS issues', async () => {
|
|
204
|
+
const scanner = new SecurityScanner({
|
|
205
|
+
path: TEST_DIR,
|
|
206
|
+
categories: ['ios']
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const result = await scanner.scan();
|
|
210
|
+
|
|
211
|
+
const iosFindings = result.findings.filter(f => f.category === 'ios');
|
|
212
|
+
expect(iosFindings.length).toBeGreaterThan(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('should filter by severity', async () => {
|
|
216
|
+
const scanner = new SecurityScanner({
|
|
217
|
+
path: TEST_DIR,
|
|
218
|
+
severity: 'high' // Show critical and high
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const result = await scanner.scan();
|
|
222
|
+
|
|
223
|
+
// All findings should be critical or high
|
|
224
|
+
for (const finding of result.findings) {
|
|
225
|
+
expect(['critical', 'high']).toContain(finding.severity);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('should return findings sorted by severity', async () => {
|
|
230
|
+
const scanner = new SecurityScanner({
|
|
231
|
+
path: TEST_DIR
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const result = await scanner.scan();
|
|
235
|
+
|
|
236
|
+
const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
|
|
237
|
+
let lastIndex = -1;
|
|
238
|
+
|
|
239
|
+
for (const finding of result.findings) {
|
|
240
|
+
const currentIndex = severityOrder.indexOf(finding.severity);
|
|
241
|
+
expect(currentIndex).toBeGreaterThanOrEqual(lastIndex);
|
|
242
|
+
lastIndex = currentIndex;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('should include remediation for all findings', async () => {
|
|
247
|
+
const scanner = new SecurityScanner({
|
|
248
|
+
path: TEST_DIR
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = await scanner.scan();
|
|
252
|
+
|
|
253
|
+
for (const finding of result.findings) {
|
|
254
|
+
expect(finding.remediation).toBeDefined();
|
|
255
|
+
expect(finding.remediation.length).toBeGreaterThan(0);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('should provide correct summary counts', async () => {
|
|
260
|
+
const scanner = new SecurityScanner({
|
|
261
|
+
path: TEST_DIR
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = await scanner.scan();
|
|
265
|
+
|
|
266
|
+
const countedTotal =
|
|
267
|
+
result.summary.critical +
|
|
268
|
+
result.summary.high +
|
|
269
|
+
result.summary.medium +
|
|
270
|
+
result.summary.low +
|
|
271
|
+
result.summary.info;
|
|
272
|
+
|
|
273
|
+
expect(countedTotal).toBe(result.summary.total);
|
|
274
|
+
expect(result.summary.total).toBe(result.findings.length);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('should have timing information', async () => {
|
|
278
|
+
const scanner = new SecurityScanner({
|
|
279
|
+
path: TEST_DIR
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const result = await scanner.scan();
|
|
283
|
+
|
|
284
|
+
expect(result.duration).toBeGreaterThan(0);
|
|
285
|
+
expect(result.timestamp).toBeDefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('getRuleCount should return correct count', () => {
|
|
289
|
+
const count = SecurityScanner.getRuleCount();
|
|
290
|
+
expect(count).toBeGreaterThan(60);
|
|
291
|
+
});
|
|
292
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"types": ["bun-types"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
19
|
+
}
|