guardrail-core 1.0.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/__tests__/autopilot.test.d.ts +7 -0
- package/dist/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/__tests__/autopilot.test.js +156 -0
- package/dist/__tests__/tier-config.test.d.ts +9 -0
- package/dist/__tests__/tier-config.test.d.ts.map +1 -0
- package/dist/__tests__/tier-config.test.js +230 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash-inline.test.js +62 -0
- package/dist/__tests__/utils/hash.test.d.ts +3 -0
- package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash.test.js +95 -0
- package/dist/__tests__/utils/simple.test.d.ts +1 -0
- package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/simple.test.js +10 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils-simple.test.js +6 -0
- package/dist/__tests__/utils/utils.test.d.ts +15 -0
- package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils.test.js +172 -0
- package/dist/autopilot/autopilot-runner.d.ts +33 -0
- package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
- package/dist/autopilot/autopilot-runner.js +479 -0
- package/dist/autopilot/index.d.ts +6 -0
- package/dist/autopilot/index.d.ts.map +1 -0
- package/dist/autopilot/index.js +25 -0
- package/dist/autopilot/types.d.ts +102 -0
- package/dist/autopilot/types.d.ts.map +1 -0
- package/dist/autopilot/types.js +18 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +22 -0
- package/dist/cache/redis-cache.d.ts +145 -0
- package/dist/cache/redis-cache.d.ts.map +1 -0
- package/dist/cache/redis-cache.js +459 -0
- package/dist/ci/github-actions.d.ts +77 -0
- package/dist/ci/github-actions.d.ts.map +1 -0
- package/dist/ci/github-actions.js +277 -0
- package/dist/ci/index.d.ts +12 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +27 -0
- package/dist/ci/pre-commit.d.ts +65 -0
- package/dist/ci/pre-commit.d.ts.map +1 -0
- package/dist/ci/pre-commit.js +286 -0
- package/dist/entitlements.d.ts +149 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +464 -0
- package/dist/env.d.ts +113 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +204 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
- package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
- package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
- package/dist/fix-packs/generate-fix-packs.js +505 -0
- package/dist/fix-packs/index.d.ts +8 -0
- package/dist/fix-packs/index.d.ts.map +1 -0
- package/dist/fix-packs/index.js +23 -0
- package/dist/fix-packs/types.d.ts +113 -0
- package/dist/fix-packs/types.d.ts.map +1 -0
- package/dist/fix-packs/types.js +71 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/metrics/prometheus.d.ts +99 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +306 -0
- package/dist/quota-ledger.d.ts +119 -0
- package/dist/quota-ledger.d.ts.map +1 -0
- package/dist/quota-ledger.js +462 -0
- package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
- package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
- package/dist/rbac/__tests__/permissions.test.js +350 -0
- package/dist/rbac/index.d.ts +9 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +32 -0
- package/dist/rbac/permissions.d.ts +71 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +247 -0
- package/dist/rbac/types.d.ts +69 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +213 -0
- package/dist/tier-config.d.ts +203 -0
- package/dist/tier-config.d.ts.map +1 -0
- package/dist/tier-config.js +675 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +127 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
- package/dist/verified-autofix/format-validator.d.ts +101 -0
- package/dist/verified-autofix/format-validator.d.ts.map +1 -0
- package/dist/verified-autofix/format-validator.js +446 -0
- package/dist/verified-autofix/index.d.ts +14 -0
- package/dist/verified-autofix/index.d.ts.map +1 -0
- package/dist/verified-autofix/index.js +39 -0
- package/dist/verified-autofix/pipeline.d.ts +68 -0
- package/dist/verified-autofix/pipeline.d.ts.map +1 -0
- package/dist/verified-autofix/pipeline.js +330 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
- package/dist/verified-autofix/repo-fingerprint.js +396 -0
- package/dist/verified-autofix/workspace.d.ts +83 -0
- package/dist/verified-autofix/workspace.d.ts.map +1 -0
- package/dist/verified-autofix/workspace.js +454 -0
- package/dist/verified-autofix.d.ts +182 -0
- package/dist/verified-autofix.d.ts.map +1 -0
- package/dist/verified-autofix.js +1021 -0
- package/dist/visualization/dependency-graph.d.ts +79 -0
- package/dist/visualization/dependency-graph.d.ts.map +1 -0
- package/dist/visualization/dependency-graph.js +399 -0
- package/dist/visualization/index.d.ts +5 -0
- package/dist/visualization/index.d.ts.map +1 -0
- package/dist/visualization/index.js +20 -0
- package/package.json +29 -0
- package/src/__tests__/autopilot.test.ts +196 -0
- package/src/__tests__/tier-config.test.ts +289 -0
- package/src/__tests__/utils/hash-inline.test.ts +76 -0
- package/src/__tests__/utils/hash.test.ts +119 -0
- package/src/__tests__/utils/simple.test.ts +10 -0
- package/src/__tests__/utils/utils-simple.test.ts +5 -0
- package/src/__tests__/utils/utils.test.ts +203 -0
- package/src/autopilot/autopilot-runner.ts +503 -0
- package/src/autopilot/index.ts +6 -0
- package/src/autopilot/types.ts +119 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/redis-cache.d.ts +155 -0
- package/src/cache/redis-cache.d.ts.map +1 -0
- package/src/cache/redis-cache.ts +517 -0
- package/src/ci/github-actions.ts +335 -0
- package/src/ci/index.ts +12 -0
- package/src/ci/pre-commit.ts +338 -0
- package/src/db/usage-schema.prisma +114 -0
- package/src/entitlements.ts +570 -0
- package/src/env.d.ts +68 -0
- package/src/env.d.ts.map +1 -0
- package/src/env.ts +247 -0
- package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
- package/src/fix-packs/generate-fix-packs.ts +577 -0
- package/src/fix-packs/index.ts +8 -0
- package/src/fix-packs/types.ts +206 -0
- package/src/index.d.ts +7 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +12 -0
- package/src/metrics/prometheus.d.ts +104 -0
- package/src/metrics/prometheus.d.ts.map +1 -0
- package/src/metrics/prometheus.ts +446 -0
- package/src/quota-ledger.ts +548 -0
- package/src/rbac/__tests__/permissions.test.ts +446 -0
- package/src/rbac/index.ts +46 -0
- package/src/rbac/permissions.ts +301 -0
- package/src/rbac/types.ts +298 -0
- package/src/tier-config.json +157 -0
- package/src/tier-config.ts +815 -0
- package/src/types.d.ts +365 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +441 -0
- package/src/utils.d.ts +36 -0
- package/src/utils.d.ts.map +1 -0
- package/src/utils.ts +140 -0
- package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
- package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
- package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
- package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
- package/src/verified-autofix/format-validator.ts +517 -0
- package/src/verified-autofix/index.ts +63 -0
- package/src/verified-autofix/pipeline.ts +403 -0
- package/src/verified-autofix/repo-fingerprint.ts +459 -0
- package/src/verified-autofix/workspace.ts +531 -0
- package/src/verified-autofix.ts +1187 -0
- package/src/visualization/dependency-graph.d.ts +85 -0
- package/src/visualization/dependency-graph.d.ts.map +1 -0
- package/src/visualization/dependency-graph.ts +495 -0
- package/src/visualization/index.ts +5 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix Packs Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates deterministic Fix Packs from findings and repo fingerprint.
|
|
5
|
+
* Groups findings by category, file proximity, and risk level.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as crypto from 'crypto';
|
|
9
|
+
import {
|
|
10
|
+
Finding,
|
|
11
|
+
FindingCategory,
|
|
12
|
+
FixPack,
|
|
13
|
+
FixStrategy,
|
|
14
|
+
RepoFingerprint,
|
|
15
|
+
GenerateFixPacksOptions,
|
|
16
|
+
GenerateFixPacksResult,
|
|
17
|
+
SeverityLevel,
|
|
18
|
+
SEVERITY_ORDER,
|
|
19
|
+
EstimatedImpact,
|
|
20
|
+
generatePackId,
|
|
21
|
+
getHighestSeverity,
|
|
22
|
+
compareSeverity,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// CONSTANTS
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
const DEFAULT_MAX_PACK_SIZE = 10;
|
|
30
|
+
const DEFAULT_MIN_PACK_SIZE = 1;
|
|
31
|
+
const FILE_PROXIMITY_THRESHOLD = 2;
|
|
32
|
+
|
|
33
|
+
const CATEGORY_PRIORITY: Record<FindingCategory, number> = {
|
|
34
|
+
secrets: 0,
|
|
35
|
+
auth: 1,
|
|
36
|
+
security: 2,
|
|
37
|
+
routes: 3,
|
|
38
|
+
mocks: 4,
|
|
39
|
+
placeholders: 5,
|
|
40
|
+
deps: 6,
|
|
41
|
+
types: 7,
|
|
42
|
+
tests: 8,
|
|
43
|
+
performance: 9,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CATEGORY_STRATEGY: Record<FindingCategory, FixStrategy> = {
|
|
47
|
+
secrets: 'auto',
|
|
48
|
+
auth: 'guided',
|
|
49
|
+
security: 'guided',
|
|
50
|
+
routes: 'auto',
|
|
51
|
+
mocks: 'auto',
|
|
52
|
+
placeholders: 'auto',
|
|
53
|
+
deps: 'guided',
|
|
54
|
+
types: 'ai-assisted',
|
|
55
|
+
tests: 'ai-assisted',
|
|
56
|
+
performance: 'manual',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const CATEGORY_REQUIRES_REVIEW: Record<FindingCategory, boolean> = {
|
|
60
|
+
secrets: false,
|
|
61
|
+
auth: true,
|
|
62
|
+
security: true,
|
|
63
|
+
routes: false,
|
|
64
|
+
mocks: false,
|
|
65
|
+
placeholders: false,
|
|
66
|
+
deps: true,
|
|
67
|
+
types: false,
|
|
68
|
+
tests: true,
|
|
69
|
+
performance: true,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// MAIN GENERATOR
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
export function generateFixPacks(options: GenerateFixPacksOptions): GenerateFixPacksResult {
|
|
77
|
+
const {
|
|
78
|
+
findings,
|
|
79
|
+
repoFingerprint,
|
|
80
|
+
groupByCategory = true,
|
|
81
|
+
groupByFileProximity = true,
|
|
82
|
+
maxPackSize = DEFAULT_MAX_PACK_SIZE,
|
|
83
|
+
minPackSize = DEFAULT_MIN_PACK_SIZE,
|
|
84
|
+
} = options;
|
|
85
|
+
|
|
86
|
+
if (findings.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
packs: [],
|
|
89
|
+
ungrouped: [],
|
|
90
|
+
stats: {
|
|
91
|
+
totalFindings: 0,
|
|
92
|
+
totalPacks: 0,
|
|
93
|
+
byCategory: {} as Record<FindingCategory, number>,
|
|
94
|
+
bySeverity: {} as Record<SeverityLevel, number>,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const sortedFindings = sortFindings(findings);
|
|
100
|
+
let grouped: Map<string, Finding[]>;
|
|
101
|
+
|
|
102
|
+
if (groupByCategory) {
|
|
103
|
+
grouped = groupByCategories(sortedFindings);
|
|
104
|
+
} else {
|
|
105
|
+
grouped = new Map([['all', sortedFindings]]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (groupByFileProximity) {
|
|
109
|
+
grouped = applyFileProximityGrouping(grouped, maxPackSize);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const packs: FixPack[] = [];
|
|
113
|
+
const ungrouped: Finding[] = [];
|
|
114
|
+
let packIndex = 0;
|
|
115
|
+
|
|
116
|
+
const sortedGroups = Array.from(grouped.entries()).sort((a, b) => {
|
|
117
|
+
const catA = a[1][0]?.category || 'performance';
|
|
118
|
+
const catB = b[1][0]?.category || 'performance';
|
|
119
|
+
return CATEGORY_PRIORITY[catA] - CATEGORY_PRIORITY[catB];
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (const [, groupFindings] of sortedGroups) {
|
|
123
|
+
if (groupFindings.length < minPackSize) {
|
|
124
|
+
ungrouped.push(...groupFindings);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const chunks = chunkFindings(groupFindings, maxPackSize);
|
|
129
|
+
|
|
130
|
+
for (const chunk of chunks) {
|
|
131
|
+
const pack = createFixPack(chunk, repoFingerprint, packIndex++);
|
|
132
|
+
packs.push(pack);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const stats = calculateStats(findings, packs);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
packs: sortPacksBySeverityAndPriority(packs),
|
|
140
|
+
ungrouped,
|
|
141
|
+
stats,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// SORTING
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
function sortFindings(findings: Finding[]): Finding[] {
|
|
150
|
+
return [...findings].sort((a, b) => {
|
|
151
|
+
const severityDiff = compareSeverity(a.severity, b.severity);
|
|
152
|
+
if (severityDiff !== 0) return severityDiff;
|
|
153
|
+
|
|
154
|
+
const categoryDiff = CATEGORY_PRIORITY[a.category] - CATEGORY_PRIORITY[b.category];
|
|
155
|
+
if (categoryDiff !== 0) return categoryDiff;
|
|
156
|
+
|
|
157
|
+
const fileDiff = a.file.localeCompare(b.file);
|
|
158
|
+
if (fileDiff !== 0) return fileDiff;
|
|
159
|
+
|
|
160
|
+
return (a.line || 0) - (b.line || 0);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sortPacksBySeverityAndPriority(packs: FixPack[]): FixPack[] {
|
|
165
|
+
return [...packs].sort((a, b) => {
|
|
166
|
+
const severityDiff = compareSeverity(a.severity, b.severity);
|
|
167
|
+
if (severityDiff !== 0) return severityDiff;
|
|
168
|
+
|
|
169
|
+
return CATEGORY_PRIORITY[a.category] - CATEGORY_PRIORITY[b.category];
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// GROUPING
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
function groupByCategories(findings: Finding[]): Map<string, Finding[]> {
|
|
178
|
+
const groups = new Map<string, Finding[]>();
|
|
179
|
+
|
|
180
|
+
for (const finding of findings) {
|
|
181
|
+
const key = finding.category;
|
|
182
|
+
if (!groups.has(key)) {
|
|
183
|
+
groups.set(key, []);
|
|
184
|
+
}
|
|
185
|
+
groups.get(key)!.push(finding);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return groups;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function applyFileProximityGrouping(
|
|
192
|
+
groups: Map<string, Finding[]>,
|
|
193
|
+
_maxPackSize: number
|
|
194
|
+
): Map<string, Finding[]> {
|
|
195
|
+
const result = new Map<string, Finding[]>();
|
|
196
|
+
|
|
197
|
+
for (const [category, findings] of groups) {
|
|
198
|
+
const fileGroups = new Map<string, Finding[]>();
|
|
199
|
+
|
|
200
|
+
for (const finding of findings) {
|
|
201
|
+
const dirPath = getDirectoryPath(finding.file, FILE_PROXIMITY_THRESHOLD);
|
|
202
|
+
if (!fileGroups.has(dirPath)) {
|
|
203
|
+
fileGroups.set(dirPath, []);
|
|
204
|
+
}
|
|
205
|
+
fileGroups.get(dirPath)!.push(finding);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let subIndex = 0;
|
|
209
|
+
for (const [dirPath, dirFindings] of fileGroups) {
|
|
210
|
+
const key = `${category}:${dirPath}:${subIndex++}`;
|
|
211
|
+
result.set(key, dirFindings);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getDirectoryPath(filePath: string, depth: number): string {
|
|
219
|
+
const parts = filePath.split(/[/\\]/);
|
|
220
|
+
if (parts.length <= depth) {
|
|
221
|
+
return parts.slice(0, -1).join('/') || '/';
|
|
222
|
+
}
|
|
223
|
+
return parts.slice(0, depth).join('/');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function chunkFindings(findings: Finding[], maxSize: number): Finding[][] {
|
|
227
|
+
const chunks: Finding[][] = [];
|
|
228
|
+
for (let i = 0; i < findings.length; i += maxSize) {
|
|
229
|
+
chunks.push(findings.slice(i, i + maxSize));
|
|
230
|
+
}
|
|
231
|
+
return chunks;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// PACK CREATION
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
function createFixPack(
|
|
239
|
+
findings: Finding[],
|
|
240
|
+
repoFingerprint: RepoFingerprint,
|
|
241
|
+
index: number
|
|
242
|
+
): FixPack {
|
|
243
|
+
const category = findings[0]?.category || 'security';
|
|
244
|
+
const severities = findings.map(f => f.severity);
|
|
245
|
+
const severity = getHighestSeverity(severities);
|
|
246
|
+
const files = [...new Set(findings.map(f => f.file))];
|
|
247
|
+
|
|
248
|
+
const packHash = generateDeterministicHash(findings, repoFingerprint);
|
|
249
|
+
const id = generatePackId(category, index, packHash);
|
|
250
|
+
|
|
251
|
+
const estimatedImpact = estimateImpact(findings, files);
|
|
252
|
+
const strategy = CATEGORY_STRATEGY[category];
|
|
253
|
+
const requiresHumanReview = CATEGORY_REQUIRES_REVIEW[category] ||
|
|
254
|
+
severity === 'critical' ||
|
|
255
|
+
estimatedImpact.riskLevel === 'high';
|
|
256
|
+
|
|
257
|
+
const title = generatePackTitle(category, findings.length, severity);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
id,
|
|
261
|
+
title,
|
|
262
|
+
severity,
|
|
263
|
+
findings,
|
|
264
|
+
files,
|
|
265
|
+
strategy,
|
|
266
|
+
estimatedImpact,
|
|
267
|
+
requiresHumanReview,
|
|
268
|
+
category,
|
|
269
|
+
createdAt: new Date().toISOString(),
|
|
270
|
+
metadata: {
|
|
271
|
+
repoFingerprint: repoFingerprint.hash,
|
|
272
|
+
generatedBy: 'guardrail-fix-packs',
|
|
273
|
+
version: '1.0.0',
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function generatePackTitle(
|
|
279
|
+
category: FindingCategory,
|
|
280
|
+
findingCount: number,
|
|
281
|
+
severity: SeverityLevel
|
|
282
|
+
): string {
|
|
283
|
+
const categoryNames: Record<FindingCategory, string> = {
|
|
284
|
+
secrets: 'Secret Exposure',
|
|
285
|
+
auth: 'Authentication Issues',
|
|
286
|
+
security: 'Security Vulnerabilities',
|
|
287
|
+
routes: 'Route Integrity',
|
|
288
|
+
mocks: 'Mock/Demo Code',
|
|
289
|
+
placeholders: 'Placeholder Content',
|
|
290
|
+
deps: 'Dependency Issues',
|
|
291
|
+
types: 'Type Errors',
|
|
292
|
+
tests: 'Test Failures',
|
|
293
|
+
performance: 'Performance Issues',
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const severityPrefix = severity === 'critical' || severity === 'high'
|
|
297
|
+
? `[${severity.toUpperCase()}] `
|
|
298
|
+
: '';
|
|
299
|
+
|
|
300
|
+
return `${severityPrefix}${categoryNames[category]} (${findingCount} ${findingCount === 1 ? 'issue' : 'issues'})`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function generateDeterministicHash(findings: Finding[], repoFingerprint: RepoFingerprint): string {
|
|
304
|
+
const data = findings.map(f => `${f.id}:${f.file}:${f.line || 0}`).sort().join('|');
|
|
305
|
+
const combined = `${repoFingerprint.hash}:${data}`;
|
|
306
|
+
return crypto.createHash('sha256').update(combined).digest('hex');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function estimateImpact(findings: Finding[], files: string[]): EstimatedImpact {
|
|
310
|
+
const filesAffected = files.length;
|
|
311
|
+
const linesChanged = findings.reduce((sum, f) => {
|
|
312
|
+
const lineSpan = (f.endLine || f.line || 1) - (f.line || 1) + 1;
|
|
313
|
+
return sum + Math.max(lineSpan, 1);
|
|
314
|
+
}, 0);
|
|
315
|
+
|
|
316
|
+
let riskLevel: 'low' | 'medium' | 'high' = 'low';
|
|
317
|
+
const hasCritical = findings.some(f => f.severity === 'critical');
|
|
318
|
+
const hasHigh = findings.some(f => f.severity === 'high');
|
|
319
|
+
|
|
320
|
+
if (hasCritical || filesAffected > 10) {
|
|
321
|
+
riskLevel = 'high';
|
|
322
|
+
} else if (hasHigh || filesAffected > 5) {
|
|
323
|
+
riskLevel = 'medium';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const confidence = calculateConfidence(findings);
|
|
327
|
+
const timeEstimateMinutes = Math.ceil(findings.length * 2 + filesAffected * 3);
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
filesAffected,
|
|
331
|
+
linesChanged,
|
|
332
|
+
riskLevel,
|
|
333
|
+
confidence,
|
|
334
|
+
timeEstimateMinutes,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function calculateConfidence(findings: Finding[]): number {
|
|
339
|
+
const hasAutoFix = findings.filter(f => f.suggestion).length;
|
|
340
|
+
const autoFixRatio = findings.length > 0 ? hasAutoFix / findings.length : 0;
|
|
341
|
+
|
|
342
|
+
const avgSeverityScore = findings.reduce((sum, f) => {
|
|
343
|
+
return sum + (4 - SEVERITY_ORDER[f.severity]);
|
|
344
|
+
}, 0) / Math.max(findings.length, 1);
|
|
345
|
+
|
|
346
|
+
return Math.min(100, Math.round((autoFixRatio * 50) + (avgSeverityScore * 10) + 20));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// STATS
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
function calculateStats(
|
|
354
|
+
findings: Finding[],
|
|
355
|
+
packs: FixPack[]
|
|
356
|
+
): GenerateFixPacksResult['stats'] {
|
|
357
|
+
const byCategory = {} as Record<FindingCategory, number>;
|
|
358
|
+
const bySeverity = {} as Record<SeverityLevel, number>;
|
|
359
|
+
|
|
360
|
+
for (const finding of findings) {
|
|
361
|
+
byCategory[finding.category] = (byCategory[finding.category] || 0) + 1;
|
|
362
|
+
bySeverity[finding.severity] = (bySeverity[finding.severity] || 0) + 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
totalFindings: findings.length,
|
|
367
|
+
totalPacks: packs.length,
|
|
368
|
+
byCategory,
|
|
369
|
+
bySeverity,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// REPO FINGERPRINT GENERATOR
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
export function generateRepoFingerprint(
|
|
378
|
+
projectPath: string,
|
|
379
|
+
options?: {
|
|
380
|
+
name?: string;
|
|
381
|
+
framework?: string;
|
|
382
|
+
language?: string;
|
|
383
|
+
}
|
|
384
|
+
): RepoFingerprint {
|
|
385
|
+
const fs = require('fs');
|
|
386
|
+
const path = require('path');
|
|
387
|
+
|
|
388
|
+
const name = options?.name || path.basename(projectPath);
|
|
389
|
+
let hasTypeScript = false;
|
|
390
|
+
let hasTests = false;
|
|
391
|
+
let packageManager: 'npm' | 'yarn' | 'pnpm' | undefined;
|
|
392
|
+
let gitRemote: string | undefined;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
hasTypeScript = fs.existsSync(path.join(projectPath, 'tsconfig.json'));
|
|
396
|
+
} catch { /* ignore */ }
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const testDirs = ['tests', 'test', '__tests__', 'spec'];
|
|
400
|
+
hasTests = testDirs.some(dir => fs.existsSync(path.join(projectPath, dir)));
|
|
401
|
+
} catch { /* ignore */ }
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
if (fs.existsSync(path.join(projectPath, 'pnpm-lock.yaml'))) {
|
|
405
|
+
packageManager = 'pnpm';
|
|
406
|
+
} else if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) {
|
|
407
|
+
packageManager = 'yarn';
|
|
408
|
+
} else if (fs.existsSync(path.join(projectPath, 'package-lock.json'))) {
|
|
409
|
+
packageManager = 'npm';
|
|
410
|
+
}
|
|
411
|
+
} catch { /* ignore */ }
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const { execSync } = require('child_process');
|
|
415
|
+
gitRemote = execSync('git remote get-url origin', {
|
|
416
|
+
cwd: projectPath,
|
|
417
|
+
encoding: 'utf8',
|
|
418
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
419
|
+
}).trim();
|
|
420
|
+
} catch { /* ignore */ }
|
|
421
|
+
|
|
422
|
+
const fingerprintData = `${name}:${hasTypeScript}:${hasTests}:${packageManager || ''}:${gitRemote || ''}`;
|
|
423
|
+
const hash = crypto.createHash('sha256').update(fingerprintData).digest('hex').slice(0, 12);
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
id: `repo-${hash}`,
|
|
427
|
+
name,
|
|
428
|
+
framework: options?.framework,
|
|
429
|
+
language: options?.language || (hasTypeScript ? 'typescript' : 'javascript'),
|
|
430
|
+
hasTypeScript,
|
|
431
|
+
hasTests,
|
|
432
|
+
packageManager,
|
|
433
|
+
gitRemote,
|
|
434
|
+
hash,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// FINDING PARSERS
|
|
440
|
+
// ============================================================================
|
|
441
|
+
|
|
442
|
+
export function parseFindingsFromScanOutput(scanOutput: string): Finding[] {
|
|
443
|
+
const findings: Finding[] = [];
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const json = JSON.parse(scanOutput);
|
|
447
|
+
if (Array.isArray(json.findings)) {
|
|
448
|
+
return json.findings.map((f: any, i: number) => normalizeFinding(f, i));
|
|
449
|
+
}
|
|
450
|
+
if (Array.isArray(json.issues)) {
|
|
451
|
+
return json.issues.map((f: any, i: number) => normalizeFinding(f, i));
|
|
452
|
+
}
|
|
453
|
+
if (Array.isArray(json)) {
|
|
454
|
+
return json.map((f: any, i: number) => normalizeFinding(f, i));
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
return parseTextFindings(scanOutput);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return findings;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function normalizeFinding(raw: any, index: number): Finding {
|
|
464
|
+
return {
|
|
465
|
+
id: raw.id || raw.ruleId || `finding-${index}`,
|
|
466
|
+
category: normalizeCategory(raw.category || raw.type || 'security'),
|
|
467
|
+
severity: normalizeSeverity(raw.severity || raw.level || 'medium'),
|
|
468
|
+
title: raw.title || raw.message || raw.description || 'Unknown issue',
|
|
469
|
+
description: raw.description || raw.message || '',
|
|
470
|
+
file: raw.file || raw.filePath || raw.path || 'unknown',
|
|
471
|
+
line: raw.line || raw.startLine || raw.location?.line,
|
|
472
|
+
column: raw.column || raw.startColumn || raw.location?.column,
|
|
473
|
+
endLine: raw.endLine || raw.location?.endLine,
|
|
474
|
+
endColumn: raw.endColumn || raw.location?.endColumn,
|
|
475
|
+
code: raw.code || raw.source,
|
|
476
|
+
suggestion: raw.suggestion || raw.fix || raw.recommendation,
|
|
477
|
+
rule: raw.rule || raw.ruleId,
|
|
478
|
+
metadata: raw.metadata || {},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function normalizeCategory(category: string): FindingCategory {
|
|
483
|
+
const categoryMap: Record<string, FindingCategory> = {
|
|
484
|
+
'secret': 'secrets',
|
|
485
|
+
'secrets': 'secrets',
|
|
486
|
+
'credential': 'secrets',
|
|
487
|
+
'api-key': 'secrets',
|
|
488
|
+
'route': 'routes',
|
|
489
|
+
'routes': 'routes',
|
|
490
|
+
'dead-link': 'routes',
|
|
491
|
+
'orphan': 'routes',
|
|
492
|
+
'mock': 'mocks',
|
|
493
|
+
'mocks': 'mocks',
|
|
494
|
+
'demo': 'mocks',
|
|
495
|
+
'fake': 'mocks',
|
|
496
|
+
'auth': 'auth',
|
|
497
|
+
'authentication': 'auth',
|
|
498
|
+
'authorization': 'auth',
|
|
499
|
+
'placeholder': 'placeholders',
|
|
500
|
+
'placeholders': 'placeholders',
|
|
501
|
+
'lorem': 'placeholders',
|
|
502
|
+
'todo': 'placeholders',
|
|
503
|
+
'dep': 'deps',
|
|
504
|
+
'deps': 'deps',
|
|
505
|
+
'dependency': 'deps',
|
|
506
|
+
'dependencies': 'deps',
|
|
507
|
+
'vulnerability': 'deps',
|
|
508
|
+
'type': 'types',
|
|
509
|
+
'types': 'types',
|
|
510
|
+
'typescript': 'types',
|
|
511
|
+
'test': 'tests',
|
|
512
|
+
'tests': 'tests',
|
|
513
|
+
'spec': 'tests',
|
|
514
|
+
'security': 'security',
|
|
515
|
+
'xss': 'security',
|
|
516
|
+
'injection': 'security',
|
|
517
|
+
'performance': 'performance',
|
|
518
|
+
'perf': 'performance',
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const normalized = category.toLowerCase();
|
|
522
|
+
return categoryMap[normalized] || 'security';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function normalizeSeverity(severity: string): SeverityLevel {
|
|
526
|
+
const severityMap: Record<string, SeverityLevel> = {
|
|
527
|
+
'critical': 'critical',
|
|
528
|
+
'blocker': 'critical',
|
|
529
|
+
'high': 'high',
|
|
530
|
+
'error': 'high',
|
|
531
|
+
'major': 'high',
|
|
532
|
+
'medium': 'medium',
|
|
533
|
+
'warning': 'medium',
|
|
534
|
+
'moderate': 'medium',
|
|
535
|
+
'low': 'low',
|
|
536
|
+
'minor': 'low',
|
|
537
|
+
'info': 'info',
|
|
538
|
+
'informational': 'info',
|
|
539
|
+
'note': 'info',
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const normalized = severity.toLowerCase();
|
|
543
|
+
return severityMap[normalized] || 'medium';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function parseTextFindings(text: string): Finding[] {
|
|
547
|
+
const findings: Finding[] = [];
|
|
548
|
+
const lines = text.split('\n');
|
|
549
|
+
|
|
550
|
+
const patterns = [
|
|
551
|
+
/^(.+):(\d+):(\d+):\s*(error|warning|info):\s*(.+)$/i,
|
|
552
|
+
/^(.+)\((\d+),(\d+)\):\s*(error|warning|info)\s+\w+:\s*(.+)$/i,
|
|
553
|
+
/^\s*(error|warning|info)\s+(.+)\s+in\s+(.+):(\d+)/i,
|
|
554
|
+
];
|
|
555
|
+
|
|
556
|
+
let index = 0;
|
|
557
|
+
for (const line of lines) {
|
|
558
|
+
for (const pattern of patterns) {
|
|
559
|
+
const match = line.match(pattern);
|
|
560
|
+
if (match) {
|
|
561
|
+
findings.push({
|
|
562
|
+
id: `text-finding-${index++}`,
|
|
563
|
+
category: 'security',
|
|
564
|
+
severity: normalizeSeverity(match[4] || 'medium'),
|
|
565
|
+
title: match[5] || match[2] || 'Unknown issue',
|
|
566
|
+
description: line,
|
|
567
|
+
file: match[1] || match[3] || 'unknown',
|
|
568
|
+
line: parseInt(match[2] || match[4] || '0', 10) || undefined,
|
|
569
|
+
column: parseInt(match[3] || '0', 10) || undefined,
|
|
570
|
+
});
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return findings;
|
|
577
|
+
}
|