@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.
Files changed (50) hide show
  1. package/dist/builtinRules.d.ts +3 -0
  2. package/dist/builtinRules.d.ts.map +1 -0
  3. package/dist/builtinRules.js +41 -0
  4. package/dist/builtinRules.js.map +1 -0
  5. package/dist/frameworks.d.ts +5 -0
  6. package/dist/frameworks.d.ts.map +1 -0
  7. package/dist/frameworks.js +169 -0
  8. package/dist/frameworks.js.map +1 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +22 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/reporters/html.d.ts +3 -0
  14. package/dist/reporters/html.d.ts.map +1 -0
  15. package/dist/reporters/html.js +64 -0
  16. package/dist/reporters/html.js.map +1 -0
  17. package/dist/reporters/sarif.d.ts +52 -0
  18. package/dist/reporters/sarif.d.ts.map +1 -0
  19. package/dist/reporters/sarif.js +76 -0
  20. package/dist/reporters/sarif.js.map +1 -0
  21. package/dist/scan.d.ts +5 -0
  22. package/dist/scan.d.ts.map +1 -0
  23. package/dist/scan.js +315 -0
  24. package/dist/scan.js.map +1 -0
  25. package/dist/types.d.ts +77 -0
  26. package/dist/types.d.ts.map +1 -0
  27. package/dist/types.js +3 -0
  28. package/dist/types.js.map +1 -0
  29. package/package.json +18 -0
  30. package/src/builtinRules.ts +39 -0
  31. package/src/frameworks.ts +202 -0
  32. package/src/index.ts +5 -0
  33. package/src/picomatch.d.ts +10 -0
  34. package/src/reporters/html.ts +65 -0
  35. package/src/reporters/sarif.ts +115 -0
  36. package/src/scan.ts +379 -0
  37. package/src/types.ts +90 -0
  38. package/test/fixtures/monorepo/apps/api/next.config.js +3 -0
  39. package/test/fixtures/monorepo/apps/api/package.json +7 -0
  40. package/test/fixtures/monorepo/apps/kit/package.json +7 -0
  41. package/test/fixtures/monorepo/apps/kit/svelte.config.js +9 -0
  42. package/test/fixtures/monorepo/apps/web/next-env.d.ts +1 -0
  43. package/test/fixtures/monorepo/apps/web/next.config.js +3 -0
  44. package/test/fixtures/monorepo/apps/web/package.json +7 -0
  45. package/test/fixtures/sample-repo/.env +2 -0
  46. package/test/fixtures/sample-repo/keys.txt +3 -0
  47. package/test/fixtures/sample-repo/src/index.ts +1 -0
  48. package/test/frameworksWorkspace.test.js +15 -0
  49. package/test/scanProject.test.js +15 -0
  50. package/tsconfig.json +8 -0
@@ -0,0 +1,202 @@
1
+ import { readFile, stat } from 'node:fs/promises'
2
+ import type { Stats } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ import fg from 'fast-glob'
6
+
7
+ import type { FrameworkDetection, FrameworkId } from './types'
8
+
9
+ type PackageJson = {
10
+ dependencies?: Record<string, string>
11
+ devDependencies?: Record<string, string>
12
+ }
13
+
14
+ const WORKSPACE_IGNORES = [
15
+ '**/.git/**',
16
+ '**/node_modules/**',
17
+ '**/dist/**',
18
+ '**/build/**',
19
+ '**/coverage/**',
20
+ '**/.next/**',
21
+ '**/.turbo/**',
22
+ '**/.cache/**',
23
+ '**/.yarn/**',
24
+ '**/.pnpm/**',
25
+ ]
26
+
27
+ async function pathStat(p: string): Promise<Stats | null> {
28
+ try {
29
+ return await stat(p)
30
+ } catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ async function hasFile(rootDir: string, relativePath: string): Promise<boolean> {
36
+ const fileStat = await pathStat(path.join(rootDir, relativePath))
37
+ return fileStat?.isFile() ?? false
38
+ }
39
+
40
+ async function hasDir(rootDir: string, relativePath: string): Promise<boolean> {
41
+ const dirStat = await pathStat(path.join(rootDir, relativePath))
42
+ return dirStat?.isDirectory() ?? false
43
+ }
44
+
45
+ async function readPackageJson(rootDir: string): Promise<PackageJson | null> {
46
+ const packageJsonPath = path.join(rootDir, 'package.json')
47
+ const fileStat = await pathStat(packageJsonPath)
48
+ if (!fileStat?.isFile()) return null
49
+
50
+ try {
51
+ const raw = await readFile(packageJsonPath, 'utf8')
52
+ return JSON.parse(raw) as PackageJson
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ function packageHasDep(pkg: PackageJson | null, name: string): boolean {
59
+ if (!pkg) return false
60
+ return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name])
61
+ }
62
+
63
+ function pushIf(value: string, condition: boolean, into: string[]) {
64
+ if (condition) into.push(value)
65
+ }
66
+
67
+ function confidenceFromEvidenceCount(count: number): FrameworkDetection['confidence'] {
68
+ if (count >= 3) return 'high'
69
+ if (count >= 2) return 'medium'
70
+ return 'low'
71
+ }
72
+
73
+ function makeDetection(id: FrameworkId, evidence: string[]): FrameworkDetection {
74
+ return {
75
+ id,
76
+ confidence: confidenceFromEvidenceCount(evidence.length),
77
+ evidence,
78
+ }
79
+ }
80
+
81
+ function sortFrameworks(frameworks: FrameworkDetection[]): void {
82
+ frameworks.sort((a, b) => {
83
+ const score = (d: FrameworkDetection) =>
84
+ d.confidence === 'high' ? 3 : d.confidence === 'medium' ? 2 : 1
85
+ return score(b) - score(a)
86
+ })
87
+ }
88
+
89
+ export async function detectFrameworks(rootDir: string): Promise<FrameworkDetection[]> {
90
+ const pkg = await readPackageJson(rootDir)
91
+
92
+ const hasNextDep = packageHasDep(pkg, 'next')
93
+ const hasNextEnv = await hasFile(rootDir, 'next-env.d.ts')
94
+
95
+ const nextEvidence: string[] = []
96
+ pushIf('dependency: next', hasNextDep, nextEvidence)
97
+ pushIf('file: next-env.d.ts', hasNextEnv, nextEvidence)
98
+ pushIf('dir: app/', await hasDir(rootDir, 'app'), nextEvidence)
99
+ pushIf('dir: pages/', await hasDir(rootDir, 'pages'), nextEvidence)
100
+
101
+ const nextConfigFiles = ['next.config.js', 'next.config.mjs', 'next.config.cjs', 'next.config.ts']
102
+ for (const f of nextConfigFiles) {
103
+ pushIf(`file: ${f}`, await hasFile(rootDir, f), nextEvidence)
104
+ }
105
+
106
+ const hasReactNativeDep = packageHasDep(pkg, 'react-native')
107
+
108
+ const rnEvidence: string[] = []
109
+ pushIf('dependency: react-native', hasReactNativeDep, rnEvidence)
110
+ pushIf('dir: ios/', await hasDir(rootDir, 'ios'), rnEvidence)
111
+ pushIf('dir: android/', await hasDir(rootDir, 'android'), rnEvidence)
112
+ pushIf('file: metro.config.js', await hasFile(rootDir, 'metro.config.js'), rnEvidence)
113
+
114
+ const expoEvidence: string[] = []
115
+ pushIf('dependency: expo', packageHasDep(pkg, 'expo'), expoEvidence)
116
+ pushIf('file: app.json', await hasFile(rootDir, 'app.json'), expoEvidence)
117
+ pushIf('file: app.config.js', await hasFile(rootDir, 'app.config.js'), expoEvidence)
118
+ pushIf('file: app.config.ts', await hasFile(rootDir, 'app.config.ts'), expoEvidence)
119
+ pushIf('file: eas.json', await hasFile(rootDir, 'eas.json'), expoEvidence)
120
+
121
+ const expressEvidence: string[] = []
122
+ pushIf('dependency: express', packageHasDep(pkg, 'express'), expressEvidence)
123
+
124
+ const hasSvelteKitDep = packageHasDep(pkg, '@sveltejs/kit')
125
+
126
+ const kitEvidence: string[] = []
127
+ pushIf('dependency: @sveltejs/kit', hasSvelteKitDep, kitEvidence)
128
+ pushIf('file: svelte.config.js', await hasFile(rootDir, 'svelte.config.js'), kitEvidence)
129
+ pushIf('file: svelte.config.ts', await hasFile(rootDir, 'svelte.config.ts'), kitEvidence)
130
+ pushIf('dir: src/routes/', await hasDir(rootDir, path.join('src', 'routes')), kitEvidence)
131
+
132
+ const frameworks: FrameworkDetection[] = []
133
+
134
+ if (hasNextDep || hasNextEnv) frameworks.push(makeDetection('nextjs', nextEvidence))
135
+
136
+ if (hasReactNativeDep) {
137
+ const combined = Array.from(new Set([...rnEvidence, ...expoEvidence]))
138
+ frameworks.push(makeDetection('react-native', combined))
139
+ }
140
+
141
+ if (expoEvidence.length > 0) frameworks.push(makeDetection('expo', expoEvidence))
142
+
143
+ if (expressEvidence.length > 0) frameworks.push(makeDetection('express', expressEvidence))
144
+
145
+ if (hasSvelteKitDep) frameworks.push(makeDetection('sveltekit', kitEvidence))
146
+
147
+ sortFrameworks(frameworks)
148
+
149
+ return frameworks
150
+ }
151
+
152
+ export async function listWorkspaceProjectRoots(rootDir: string): Promise<string[]> {
153
+ const packageJsonPaths = await fg('**/package.json', {
154
+ cwd: rootDir,
155
+ dot: true,
156
+ onlyFiles: true,
157
+ followSymbolicLinks: false,
158
+ ignore: WORKSPACE_IGNORES,
159
+ })
160
+
161
+ const resolvedRoot = path.resolve(rootDir)
162
+
163
+ const roots = Array.from(
164
+ new Set(packageJsonPaths.map((relativePath) => path.join(rootDir, path.dirname(relativePath)))),
165
+ )
166
+ .map((p) => path.resolve(p))
167
+ .filter((p) => p !== resolvedRoot)
168
+
169
+ roots.sort()
170
+
171
+ return roots
172
+ }
173
+
174
+ export async function detectFrameworksInWorkspace(rootDir: string): Promise<FrameworkDetection[]> {
175
+ const roots = await listWorkspaceProjectRoots(rootDir)
176
+
177
+ const byFramework = new Map<FrameworkId, Set<string>>()
178
+
179
+ for (const projectRoot of roots) {
180
+ const detections = await detectFrameworks(projectRoot)
181
+ if (detections.length === 0) continue
182
+
183
+ const relativeRoot = path.relative(rootDir, projectRoot) || '.'
184
+ for (const detection of detections) {
185
+ const existing = byFramework.get(detection.id) ?? new Set<string>()
186
+ for (const evidence of detection.evidence) {
187
+ existing.add(`${relativeRoot}: ${evidence}`)
188
+ }
189
+ byFramework.set(detection.id, existing)
190
+ }
191
+ }
192
+
193
+ const frameworks: FrameworkDetection[] = []
194
+
195
+ for (const [id, evidenceSet] of byFramework.entries()) {
196
+ frameworks.push(makeDetection(id, Array.from(evidenceSet)))
197
+ }
198
+
199
+ sortFrameworks(frameworks)
200
+
201
+ return frameworks
202
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './frameworks'
2
+ export * from './reporters/html'
3
+ export * from './reporters/sarif'
4
+ export * from './scan'
5
+ export * from './types'
@@ -0,0 +1,10 @@
1
+ declare module 'picomatch' {
2
+ export type PicomatchOptions = {
3
+ dot?: boolean
4
+ }
5
+
6
+ export default function picomatch(
7
+ patterns: string | string[],
8
+ options?: PicomatchOptions,
9
+ ): (path: string) => boolean
10
+ }
@@ -0,0 +1,65 @@
1
+ import type { ScanResult } from '../types'
2
+
3
+ function escapeHtml(input: string): string {
4
+ return input
5
+ .replaceAll('&', '&amp;')
6
+ .replaceAll('<', '&lt;')
7
+ .replaceAll('>', '&gt;')
8
+ .replaceAll('"', '&quot;')
9
+ .replaceAll("'", '&#39;')
10
+ }
11
+
12
+ export function toHtml(result: ScanResult): string {
13
+ const frameworks = result.frameworks.map((f) => escapeHtml(f.id)).join(', ')
14
+ const findings = result.findings
15
+ .map((f) => {
16
+ const location = `${escapeHtml(f.location.path)}:${f.location.startLine}`
17
+ const title = escapeHtml(f.ruleTitle)
18
+ const message = escapeHtml(f.message)
19
+ const severity = escapeHtml(f.severity.toUpperCase())
20
+
21
+ return `
22
+ <div class="finding">
23
+ <div class="finding__header">
24
+ <span class="badge badge--${f.severity}">${severity}</span>
25
+ <span class="finding__rule">${escapeHtml(f.ruleId)}</span>
26
+ </div>
27
+ <div class="finding__title">${title}</div>
28
+ <div class="finding__location">${location}</div>
29
+ <div class="finding__message">${message}</div>
30
+ </div>
31
+ `
32
+ })
33
+ .join('\n')
34
+
35
+ return `<!doctype html>
36
+ <html lang="en">
37
+ <head>
38
+ <meta charset="utf-8" />
39
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
40
+ <title>VibeSec report</title>
41
+ <style>
42
+ body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; margin: 24px; color: #e5e7eb; background: #0b1220; }
43
+ h1 { margin: 0 0 8px 0; }
44
+ .meta { color: #9ca3af; margin-bottom: 16px; }
45
+ .finding { border: 1px solid #1f2937; border-radius: 10px; padding: 12px; margin: 12px 0; background: #0f172a; }
46
+ .finding__header { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; }
47
+ .finding__rule { color: #cbd5e1; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"; font-size: 12px; }
48
+ .finding__title { font-weight: 600; margin-bottom: 6px; }
49
+ .finding__location { color: #9ca3af; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"; font-size: 12px; margin-bottom: 6px; }
50
+ .finding__message { color: #e5e7eb; }
51
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .02em; }
52
+ .badge--critical { background: #7f1d1d; color: #fecaca; }
53
+ .badge--high { background: #9a3412; color: #ffedd5; }
54
+ .badge--medium { background: #92400e; color: #fef3c7; }
55
+ .badge--low { background: #1f2937; color: #e5e7eb; }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <h1>VibeSec report</h1>
60
+ <div class="meta">Frameworks: ${frameworks || 'none'} · Findings: ${result.findings.length}</div>
61
+ ${findings || '<div class="meta">No findings.</div>'}
62
+ </body>
63
+ </html>
64
+ `
65
+ }
@@ -0,0 +1,115 @@
1
+ import type { Finding, ScanResult, SeverityName } from '../types'
2
+
3
+ type SarifLevel = 'error' | 'warning' | 'note'
4
+
5
+ type SarifReport = {
6
+ version: '2.1.0'
7
+ $schema: string
8
+ runs: Array<{
9
+ tool: {
10
+ driver: {
11
+ name: string
12
+ version: string
13
+ informationUri?: string
14
+ rules?: Array<{
15
+ id: string
16
+ name?: string
17
+ shortDescription: { text: string }
18
+ fullDescription?: { text: string }
19
+ help?: { text: string }
20
+ properties?: Record<string, unknown>
21
+ }>
22
+ }
23
+ }
24
+ results: Array<{
25
+ ruleId: string
26
+ level: SarifLevel
27
+ message: { text: string }
28
+ locations: Array<{
29
+ physicalLocation: {
30
+ artifactLocation: { uri: string }
31
+ region: { startLine: number; startColumn: number }
32
+ }
33
+ }>
34
+ partialFingerprints?: Record<string, string>
35
+ properties?: Record<string, unknown>
36
+ }>
37
+ }>
38
+ }
39
+
40
+ function sarifLevel(severity: SeverityName): SarifLevel {
41
+ switch (severity) {
42
+ case 'critical':
43
+ case 'high':
44
+ return 'error'
45
+ case 'medium':
46
+ return 'warning'
47
+ case 'low':
48
+ return 'note'
49
+ }
50
+ }
51
+
52
+ function ruleKey(finding: Finding): string {
53
+ return finding.ruleId
54
+ }
55
+
56
+ export function toSarif(result: ScanResult): SarifReport {
57
+ const rulesById = new Map<string, Finding>()
58
+ for (const finding of result.findings) {
59
+ const id = ruleKey(finding)
60
+ if (!rulesById.has(id)) rulesById.set(id, finding)
61
+ }
62
+
63
+ const rules = Array.from(rulesById.values()).map((finding) => ({
64
+ id: finding.ruleId,
65
+ name: finding.ruleId,
66
+ shortDescription: { text: finding.ruleTitle },
67
+ fullDescription: finding.ruleDescription ? { text: finding.ruleDescription } : undefined,
68
+ help: { text: finding.message },
69
+ properties: {
70
+ severity: finding.severity,
71
+ },
72
+ }))
73
+
74
+ const results = result.findings.map((finding) => ({
75
+ ruleId: finding.ruleId,
76
+ level: sarifLevel(finding.severity),
77
+ message: { text: finding.message },
78
+ locations: [
79
+ {
80
+ physicalLocation: {
81
+ artifactLocation: { uri: finding.location.path },
82
+ region: {
83
+ startLine: finding.location.startLine,
84
+ startColumn: finding.location.startColumn,
85
+ },
86
+ },
87
+ },
88
+ ],
89
+ partialFingerprints: {
90
+ 'vibesec/fingerprint': finding.fingerprint,
91
+ },
92
+ properties: {
93
+ severity: finding.severity,
94
+ fingerprint: finding.fingerprint,
95
+ },
96
+ }))
97
+
98
+ return {
99
+ version: '2.1.0',
100
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
101
+ runs: [
102
+ {
103
+ tool: {
104
+ driver: {
105
+ name: 'vibesec',
106
+ version: '0.0.0',
107
+ informationUri: 'https://github.com/Reliability-Works/vibesec',
108
+ rules,
109
+ },
110
+ },
111
+ results,
112
+ },
113
+ ],
114
+ }
115
+ }