@reliabilityworks/core 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/dist/builtinRules.d.ts +3 -0
- package/dist/builtinRules.d.ts.map +1 -0
- package/dist/builtinRules.js +41 -0
- package/dist/builtinRules.js.map +1 -0
- package/dist/frameworks.d.ts +5 -0
- package/dist/frameworks.d.ts.map +1 -0
- package/dist/frameworks.js +169 -0
- package/dist/frameworks.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/reporters/html.d.ts +3 -0
- package/dist/reporters/html.d.ts.map +1 -0
- package/dist/reporters/html.js +64 -0
- package/dist/reporters/html.js.map +1 -0
- package/dist/reporters/sarif.d.ts +52 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +76 -0
- package/dist/reporters/sarif.js.map +1 -0
- package/dist/scan.d.ts +5 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +315 -0
- package/dist/scan.js.map +1 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +18 -0
- package/src/builtinRules.ts +39 -0
- package/src/frameworks.ts +202 -0
- package/src/index.ts +5 -0
- package/src/picomatch.d.ts +10 -0
- package/src/reporters/html.ts +65 -0
- package/src/reporters/sarif.ts +115 -0
- package/src/scan.ts +379 -0
- package/src/types.ts +90 -0
- package/test/fixtures/monorepo/apps/api/next.config.js +3 -0
- package/test/fixtures/monorepo/apps/api/package.json +7 -0
- package/test/fixtures/monorepo/apps/kit/package.json +7 -0
- package/test/fixtures/monorepo/apps/kit/svelte.config.js +9 -0
- package/test/fixtures/monorepo/apps/web/next-env.d.ts +1 -0
- package/test/fixtures/monorepo/apps/web/next.config.js +3 -0
- package/test/fixtures/monorepo/apps/web/package.json +7 -0
- package/test/fixtures/sample-repo/.env +2 -0
- package/test/fixtures/sample-repo/keys.txt +3 -0
- package/test/fixtures/sample-repo/src/index.ts +1 -0
- package/test/frameworksWorkspace.test.js +15 -0
- package/test/scanProject.test.js +15 -0
- package/tsconfig.json +8 -0
package/src/scan.ts
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import fg from 'fast-glob'
|
|
6
|
+
import picomatch from 'picomatch'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import YAML from 'yaml'
|
|
9
|
+
|
|
10
|
+
import { BUILTIN_RULES } from './builtinRules'
|
|
11
|
+
import { detectFrameworks } from './frameworks'
|
|
12
|
+
import type {
|
|
13
|
+
Finding,
|
|
14
|
+
FindingLocation,
|
|
15
|
+
Rule,
|
|
16
|
+
ScanOptions,
|
|
17
|
+
ScanResult,
|
|
18
|
+
Severity,
|
|
19
|
+
SeverityName,
|
|
20
|
+
VibeSecConfig,
|
|
21
|
+
} from './types'
|
|
22
|
+
|
|
23
|
+
const DEFAULT_IGNORES = [
|
|
24
|
+
'**/.git/**',
|
|
25
|
+
'**/node_modules/**',
|
|
26
|
+
'**/dist/**',
|
|
27
|
+
'**/build/**',
|
|
28
|
+
'**/coverage/**',
|
|
29
|
+
'**/.next/**',
|
|
30
|
+
'**/.turbo/**',
|
|
31
|
+
'**/.cache/**',
|
|
32
|
+
'**/.yarn/**',
|
|
33
|
+
'**/.pnpm/**',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const DEFAULT_MAX_FILE_SIZE_BYTES = 1024 * 1024
|
|
37
|
+
|
|
38
|
+
export type SeverityNameInput = SeverityName
|
|
39
|
+
|
|
40
|
+
export function severityFromString(name: SeverityNameInput): Severity {
|
|
41
|
+
switch (name) {
|
|
42
|
+
case 'critical':
|
|
43
|
+
return { name, rank: 0 }
|
|
44
|
+
case 'high':
|
|
45
|
+
return { name, rank: 1 }
|
|
46
|
+
case 'medium':
|
|
47
|
+
return { name, rank: 2 }
|
|
48
|
+
case 'low':
|
|
49
|
+
return { name, rank: 3 }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fileExists(p: string): Promise<boolean> {
|
|
54
|
+
return fs
|
|
55
|
+
.stat(p)
|
|
56
|
+
.then(() => true)
|
|
57
|
+
.catch(() => false)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sha256Hex(input: string): string {
|
|
61
|
+
return crypto.createHash('sha256').update(input).digest('hex')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isLikelyBinary(buffer: Buffer): boolean {
|
|
65
|
+
for (const b of buffer) {
|
|
66
|
+
if (b === 0) return true
|
|
67
|
+
}
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function computeLineInfo(
|
|
72
|
+
text: string,
|
|
73
|
+
matchIndex: number,
|
|
74
|
+
): {
|
|
75
|
+
lineNumber: number
|
|
76
|
+
columnNumber: number
|
|
77
|
+
lineText: string
|
|
78
|
+
} {
|
|
79
|
+
const upToMatch = text.slice(0, matchIndex)
|
|
80
|
+
const lines = upToMatch.split('\n')
|
|
81
|
+
const lineNumber = lines.length
|
|
82
|
+
const columnNumber = lines[lines.length - 1]?.length ?? 0
|
|
83
|
+
|
|
84
|
+
const fullLines = text.split('\n')
|
|
85
|
+
const lineText = fullLines[lineNumber - 1] ?? ''
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
lineNumber,
|
|
89
|
+
columnNumber: columnNumber + 1,
|
|
90
|
+
lineText,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fingerprintForMatch(args: {
|
|
95
|
+
ruleId: string
|
|
96
|
+
relativePath: string
|
|
97
|
+
matchText?: string
|
|
98
|
+
lineText?: string
|
|
99
|
+
}): string {
|
|
100
|
+
const material = [
|
|
101
|
+
`rule:${args.ruleId}`,
|
|
102
|
+
`path:${args.relativePath}`,
|
|
103
|
+
args.matchText ? `match:${args.matchText}` : undefined,
|
|
104
|
+
args.lineText ? `line:${args.lineText.trim()}` : undefined,
|
|
105
|
+
]
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join('\n')
|
|
108
|
+
|
|
109
|
+
return sha256Hex(material)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const ignoreEntrySchema = z.union([
|
|
113
|
+
z.object({
|
|
114
|
+
rule: z.string().min(1),
|
|
115
|
+
reason: z.string().min(1),
|
|
116
|
+
paths: z.array(z.string().min(1)).optional(),
|
|
117
|
+
}),
|
|
118
|
+
z.object({
|
|
119
|
+
finding: z.string().min(1),
|
|
120
|
+
reason: z.string().min(1),
|
|
121
|
+
}),
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
const configSchema = z.object({
|
|
125
|
+
ignore: z.array(ignoreEntrySchema).optional(),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
function loadConfig(configRootDir: string, configPath?: string): Promise<VibeSecConfig> {
|
|
129
|
+
const candidates = configPath
|
|
130
|
+
? [configPath]
|
|
131
|
+
: [path.join(configRootDir, '.vibesec.yaml'), path.join(configRootDir, '.vibesec.yml')]
|
|
132
|
+
|
|
133
|
+
return (async () => {
|
|
134
|
+
for (const candidate of candidates) {
|
|
135
|
+
if (!(await fileExists(candidate))) continue
|
|
136
|
+
const raw = await fs.readFile(candidate, 'utf8')
|
|
137
|
+
const parsed = YAML.parse(raw)
|
|
138
|
+
const validated = configSchema.safeParse(parsed)
|
|
139
|
+
if (!validated.success) {
|
|
140
|
+
throw new Error(`Invalid config at ${candidate}`)
|
|
141
|
+
}
|
|
142
|
+
return validated.data
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {}
|
|
146
|
+
})()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ruleSchema: z.ZodType<Rule> = z.object({
|
|
150
|
+
id: z.string().min(1),
|
|
151
|
+
severity: z.union([
|
|
152
|
+
z.literal('critical'),
|
|
153
|
+
z.literal('high'),
|
|
154
|
+
z.literal('medium'),
|
|
155
|
+
z.literal('low'),
|
|
156
|
+
]),
|
|
157
|
+
title: z.string().min(1),
|
|
158
|
+
description: z.string().optional(),
|
|
159
|
+
matcher: z.union([
|
|
160
|
+
z.object({
|
|
161
|
+
type: z.literal('file_presence'),
|
|
162
|
+
paths: z.array(z.string().min(1)).min(1),
|
|
163
|
+
message: z.string().min(1),
|
|
164
|
+
}),
|
|
165
|
+
z.object({
|
|
166
|
+
type: z.literal('regex'),
|
|
167
|
+
fileGlobs: z.array(z.string().min(1)).min(1),
|
|
168
|
+
pattern: z.string().min(1),
|
|
169
|
+
flags: z.string().optional(),
|
|
170
|
+
message: z.string().min(1),
|
|
171
|
+
}),
|
|
172
|
+
]),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
async function loadCustomRules(configRootDir: string, customRulesDir?: string): Promise<Rule[]> {
|
|
176
|
+
const rulesDir = customRulesDir ?? path.join(configRootDir, '.vibesec', 'rules')
|
|
177
|
+
if (!(await fileExists(rulesDir))) return []
|
|
178
|
+
|
|
179
|
+
const entries = await fs.readdir(rulesDir, { withFileTypes: true })
|
|
180
|
+
const ruleFiles = entries
|
|
181
|
+
.filter((e) => e.isFile())
|
|
182
|
+
.map((e) => e.name)
|
|
183
|
+
.filter((name) => name.endsWith('.yml') || name.endsWith('.yaml') || name.endsWith('.json'))
|
|
184
|
+
|
|
185
|
+
const rules: Rule[] = []
|
|
186
|
+
|
|
187
|
+
for (const fileName of ruleFiles) {
|
|
188
|
+
const fullPath = path.join(rulesDir, fileName)
|
|
189
|
+
const raw = await fs.readFile(fullPath, 'utf8')
|
|
190
|
+
|
|
191
|
+
const parsed = fileName.endsWith('.json') ? JSON.parse(raw) : YAML.parse(raw)
|
|
192
|
+
const items = Array.isArray(parsed) ? parsed : [parsed]
|
|
193
|
+
|
|
194
|
+
for (const item of items) {
|
|
195
|
+
const validated = ruleSchema.safeParse(item)
|
|
196
|
+
if (!validated.success) {
|
|
197
|
+
throw new Error(`Invalid custom rule in ${fullPath}`)
|
|
198
|
+
}
|
|
199
|
+
rules.push(validated.data)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return rules
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isIgnored(config: VibeSecConfig, finding: Finding): boolean {
|
|
207
|
+
const ignores = config.ignore ?? []
|
|
208
|
+
|
|
209
|
+
for (const entry of ignores) {
|
|
210
|
+
if ('finding' in entry) {
|
|
211
|
+
if (entry.finding === finding.fingerprint) return true
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (entry.rule !== finding.ruleId) continue
|
|
216
|
+
|
|
217
|
+
if (!entry.paths || entry.paths.length === 0) return true
|
|
218
|
+
const matchesPath = picomatch(entry.paths, { dot: true })
|
|
219
|
+
if (matchesPath(finding.location.path)) return true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function listProjectFiles(rootDir: string): Promise<string[]> {
|
|
226
|
+
return fg('**/*', {
|
|
227
|
+
cwd: rootDir,
|
|
228
|
+
dot: true,
|
|
229
|
+
onlyFiles: true,
|
|
230
|
+
followSymbolicLinks: false,
|
|
231
|
+
ignore: DEFAULT_IGNORES,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function readTextFileIfSafe(fullPath: string, maxBytes: number): Promise<string | null> {
|
|
236
|
+
const stat = await fs.stat(fullPath)
|
|
237
|
+
if (stat.size > maxBytes) return null
|
|
238
|
+
|
|
239
|
+
const handle = await fs.open(fullPath, 'r')
|
|
240
|
+
try {
|
|
241
|
+
const probeSize = Math.min(stat.size, 4096)
|
|
242
|
+
const probe = Buffer.alloc(probeSize)
|
|
243
|
+
await handle.read(probe, 0, probeSize, 0)
|
|
244
|
+
if (isLikelyBinary(probe)) return null
|
|
245
|
+
|
|
246
|
+
return await handle.readFile({ encoding: 'utf8' })
|
|
247
|
+
} finally {
|
|
248
|
+
await handle.close()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function makeFinding(args: {
|
|
253
|
+
rule: Rule
|
|
254
|
+
location: FindingLocation
|
|
255
|
+
message: string
|
|
256
|
+
excerpt?: string
|
|
257
|
+
matchText?: string
|
|
258
|
+
lineText?: string
|
|
259
|
+
}): Finding {
|
|
260
|
+
const severity = severityFromString(args.rule.severity)
|
|
261
|
+
const fingerprint = fingerprintForMatch({
|
|
262
|
+
ruleId: args.rule.id,
|
|
263
|
+
relativePath: args.location.path,
|
|
264
|
+
matchText: args.matchText,
|
|
265
|
+
lineText: args.lineText,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
ruleId: args.rule.id,
|
|
270
|
+
ruleTitle: args.rule.title,
|
|
271
|
+
ruleDescription: args.rule.description,
|
|
272
|
+
severity: args.rule.severity,
|
|
273
|
+
severityRank: severity.rank,
|
|
274
|
+
message: args.message,
|
|
275
|
+
location: args.location,
|
|
276
|
+
fingerprint,
|
|
277
|
+
excerpt: args.excerpt,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function scanProject(options: ScanOptions): Promise<ScanResult> {
|
|
282
|
+
const scanDir = path.resolve(options.rootDir)
|
|
283
|
+
const configRootDir = path.resolve(options.configRootDir ?? scanDir)
|
|
284
|
+
const pathBaseDir = path.resolve(options.pathBaseDir ?? scanDir)
|
|
285
|
+
const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES
|
|
286
|
+
|
|
287
|
+
const config = await loadConfig(configRootDir, options.configPath)
|
|
288
|
+
const additionalRules = options.additionalRules ?? []
|
|
289
|
+
const rules = [
|
|
290
|
+
...BUILTIN_RULES,
|
|
291
|
+
...(await loadCustomRules(configRootDir, options.customRulesDir)),
|
|
292
|
+
...additionalRules,
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
const frameworks = options.frameworks ?? (await detectFrameworks(scanDir))
|
|
296
|
+
const files = await listProjectFiles(scanDir)
|
|
297
|
+
|
|
298
|
+
const toBasePath = (scanRelativePath: string): string => {
|
|
299
|
+
const absolutePath = path.join(scanDir, scanRelativePath)
|
|
300
|
+
const rel = path.relative(pathBaseDir, absolutePath)
|
|
301
|
+
return (rel || scanRelativePath).split(path.sep).join('/')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const findings: Finding[] = []
|
|
305
|
+
let ignoredFindings = 0
|
|
306
|
+
|
|
307
|
+
for (const rule of rules) {
|
|
308
|
+
if (rule.matcher.type === 'file_presence') {
|
|
309
|
+
const matches = files.filter(picomatch(rule.matcher.paths, { dot: true }))
|
|
310
|
+
for (const relativePath of matches) {
|
|
311
|
+
const finding = makeFinding({
|
|
312
|
+
rule,
|
|
313
|
+
location: { path: toBasePath(relativePath), startLine: 1, startColumn: 1 },
|
|
314
|
+
message: rule.matcher.message,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
if (isIgnored(config, finding)) {
|
|
318
|
+
ignoredFindings += 1
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
findings.push(finding)
|
|
323
|
+
}
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const compiled = new RegExp(rule.matcher.pattern, rule.matcher.flags)
|
|
328
|
+
const matchesFile = picomatch(rule.matcher.fileGlobs, { dot: true })
|
|
329
|
+
|
|
330
|
+
for (const relativePath of files) {
|
|
331
|
+
if (!matchesFile(relativePath)) continue
|
|
332
|
+
|
|
333
|
+
const fullPath = path.join(scanDir, relativePath)
|
|
334
|
+
let text: string | null
|
|
335
|
+
try {
|
|
336
|
+
text = await readTextFileIfSafe(fullPath, maxFileSizeBytes)
|
|
337
|
+
} catch {
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
if (!text) continue
|
|
341
|
+
|
|
342
|
+
const match = compiled.exec(text)
|
|
343
|
+
if (!match || match.index == null) continue
|
|
344
|
+
|
|
345
|
+
const { lineNumber, columnNumber, lineText } = computeLineInfo(text, match.index)
|
|
346
|
+
const excerpt = lineText.trim().slice(0, 300)
|
|
347
|
+
|
|
348
|
+
const finding = makeFinding({
|
|
349
|
+
rule,
|
|
350
|
+
location: {
|
|
351
|
+
path: toBasePath(relativePath),
|
|
352
|
+
startLine: lineNumber,
|
|
353
|
+
startColumn: columnNumber,
|
|
354
|
+
},
|
|
355
|
+
message: rule.matcher.message,
|
|
356
|
+
excerpt,
|
|
357
|
+
matchText: match[0],
|
|
358
|
+
lineText,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
if (isIgnored(config, finding)) {
|
|
362
|
+
ignoredFindings += 1
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
findings.push(finding)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
findings.sort((a, b) => a.severityRank - b.severityRank)
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
rootDir: scanDir,
|
|
374
|
+
frameworks,
|
|
375
|
+
scannedFiles: files.length,
|
|
376
|
+
ignoredFindings,
|
|
377
|
+
findings,
|
|
378
|
+
}
|
|
379
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type SeverityName = 'critical' | 'high' | 'medium' | 'low'
|
|
2
|
+
|
|
3
|
+
export type Severity = {
|
|
4
|
+
name: SeverityName
|
|
5
|
+
rank: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type FindingLocation = {
|
|
9
|
+
path: string
|
|
10
|
+
startLine: number
|
|
11
|
+
startColumn: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Finding = {
|
|
15
|
+
ruleId: string
|
|
16
|
+
ruleTitle: string
|
|
17
|
+
ruleDescription?: string
|
|
18
|
+
severity: SeverityName
|
|
19
|
+
severityRank: number
|
|
20
|
+
message: string
|
|
21
|
+
location: FindingLocation
|
|
22
|
+
fingerprint: string
|
|
23
|
+
excerpt?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type FrameworkId = 'nextjs' | 'react-native' | 'expo' | 'express' | 'sveltekit'
|
|
27
|
+
|
|
28
|
+
export type FrameworkDetection = {
|
|
29
|
+
id: FrameworkId
|
|
30
|
+
confidence: 'high' | 'medium' | 'low'
|
|
31
|
+
evidence: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ScanResult = {
|
|
35
|
+
rootDir: string
|
|
36
|
+
frameworks: FrameworkDetection[]
|
|
37
|
+
scannedFiles: number
|
|
38
|
+
ignoredFindings: number
|
|
39
|
+
findings: Finding[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ScanOptions = {
|
|
43
|
+
rootDir: string
|
|
44
|
+
pathBaseDir?: string
|
|
45
|
+
configRootDir?: string
|
|
46
|
+
configPath?: string
|
|
47
|
+
customRulesDir?: string
|
|
48
|
+
frameworks?: FrameworkDetection[]
|
|
49
|
+
additionalRules?: Rule[]
|
|
50
|
+
maxFileSizeBytes?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type IgnoreByRule = {
|
|
54
|
+
rule: string
|
|
55
|
+
reason: string
|
|
56
|
+
paths?: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type IgnoreByFinding = {
|
|
60
|
+
finding: string
|
|
61
|
+
reason: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type VibeSecConfig = {
|
|
65
|
+
ignore?: Array<IgnoreByRule | IgnoreByFinding>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type FilePresenceMatcher = {
|
|
69
|
+
type: 'file_presence'
|
|
70
|
+
paths: string[]
|
|
71
|
+
message: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type RegexMatcher = {
|
|
75
|
+
type: 'regex'
|
|
76
|
+
fileGlobs: string[]
|
|
77
|
+
pattern: string
|
|
78
|
+
flags?: string
|
|
79
|
+
message: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type RuleMatcher = FilePresenceMatcher | RegexMatcher
|
|
83
|
+
|
|
84
|
+
export type Rule = {
|
|
85
|
+
id: string
|
|
86
|
+
severity: SeverityName
|
|
87
|
+
title: string
|
|
88
|
+
description?: string
|
|
89
|
+
matcher: RuleMatcher
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const example = 'AKIA1234567890ABCDEF'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const test = require('node:test')
|
|
4
|
+
|
|
5
|
+
const { detectFrameworksInWorkspace } = require('../dist/index.js')
|
|
6
|
+
|
|
7
|
+
test('detectFrameworksInWorkspace finds nested frameworks', async () => {
|
|
8
|
+
const fixtureRoot = path.join(__dirname, 'fixtures', 'monorepo')
|
|
9
|
+
const frameworks = await detectFrameworksInWorkspace(fixtureRoot)
|
|
10
|
+
|
|
11
|
+
const ids = frameworks.map((f) => f.id)
|
|
12
|
+
assert.ok(ids.includes('nextjs'))
|
|
13
|
+
assert.ok(ids.includes('express'))
|
|
14
|
+
assert.ok(ids.includes('sveltekit'))
|
|
15
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const test = require('node:test')
|
|
4
|
+
|
|
5
|
+
const { scanProject } = require('../dist/index.js')
|
|
6
|
+
|
|
7
|
+
test('scanProject finds builtin rule matches', async () => {
|
|
8
|
+
const fixtureRoot = path.join(__dirname, 'fixtures', 'sample-repo')
|
|
9
|
+
const result = await scanProject({ rootDir: fixtureRoot })
|
|
10
|
+
|
|
11
|
+
const ruleIds = result.findings.map((f) => f.ruleId)
|
|
12
|
+
assert.ok(ruleIds.includes('core/env-file-committed'))
|
|
13
|
+
assert.ok(ruleIds.includes('core/private-key-committed'))
|
|
14
|
+
assert.ok(ruleIds.includes('core/hardcoded-aws-access-key-id'))
|
|
15
|
+
})
|