vaspera 2.9.0 → 2.10.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/CHANGELOG.md +122 -7
- package/README.md +58 -1
- package/dist/__tests__/autofix/branch-manager.test.d.ts +2 -0
- package/dist/__tests__/autofix/branch-manager.test.d.ts.map +1 -0
- package/dist/__tests__/autofix/branch-manager.test.js +60 -0
- package/dist/__tests__/autofix/branch-manager.test.js.map +1 -0
- package/dist/__tests__/autofix/commit-generator.test.d.ts +2 -0
- package/dist/__tests__/autofix/commit-generator.test.d.ts.map +1 -0
- package/dist/__tests__/autofix/commit-generator.test.js +147 -0
- package/dist/__tests__/autofix/commit-generator.test.js.map +1 -0
- package/dist/__tests__/autofix/constitution.test.d.ts +9 -0
- package/dist/__tests__/autofix/constitution.test.d.ts.map +1 -0
- package/dist/__tests__/autofix/constitution.test.js +421 -0
- package/dist/__tests__/autofix/constitution.test.js.map +1 -0
- package/dist/__tests__/autofix/pr-generator.test.d.ts +2 -0
- package/dist/__tests__/autofix/pr-generator.test.d.ts.map +1 -0
- package/dist/__tests__/autofix/pr-generator.test.js +152 -0
- package/dist/__tests__/autofix/pr-generator.test.js.map +1 -0
- package/dist/__tests__/property-test-helpers.d.ts +87 -0
- package/dist/__tests__/property-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/property-test-helpers.js +136 -0
- package/dist/__tests__/property-test-helpers.js.map +1 -0
- package/dist/__tests__/scanners/dast/index.test.d.ts +2 -0
- package/dist/__tests__/scanners/dast/index.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/dast/index.test.js +183 -0
- package/dist/__tests__/scanners/dast/index.test.js.map +1 -0
- package/dist/__tests__/scanners/dast/nuclei.test.d.ts +2 -0
- package/dist/__tests__/scanners/dast/nuclei.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/dast/nuclei.test.js +166 -0
- package/dist/__tests__/scanners/dast/nuclei.test.js.map +1 -0
- package/dist/__tests__/scanners/dast/zap.test.d.ts +2 -0
- package/dist/__tests__/scanners/dast/zap.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/dast/zap.test.js +158 -0
- package/dist/__tests__/scanners/dast/zap.test.js.map +1 -0
- package/dist/__tests__/scanners/fp-feedback.test.d.ts +2 -0
- package/dist/__tests__/scanners/fp-feedback.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/fp-feedback.test.js +202 -0
- package/dist/__tests__/scanners/fp-feedback.test.js.map +1 -0
- package/dist/__tests__/scanners/fp-filter.property.test.d.ts +9 -0
- package/dist/__tests__/scanners/fp-filter.property.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/fp-filter.property.test.js +253 -0
- package/dist/__tests__/scanners/fp-filter.property.test.js.map +1 -0
- package/dist/__tests__/scanners/fp-filter.test.d.ts +2 -0
- package/dist/__tests__/scanners/fp-filter.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/fp-filter.test.js +234 -0
- package/dist/__tests__/scanners/fp-filter.test.js.map +1 -0
- package/dist/__tests__/scanners/fp-tracker.test.d.ts +2 -0
- package/dist/__tests__/scanners/fp-tracker.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/fp-tracker.test.js +262 -0
- package/dist/__tests__/scanners/fp-tracker.test.js.map +1 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.property.test.d.ts +10 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.property.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.property.test.js +238 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.property.test.js.map +1 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.test.d.ts +2 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.test.js +55 -0
- package/dist/__tests__/scanners/logic/endpoint-analyzer.test.js.map +1 -0
- package/dist/__tests__/scanners/logic/index.test.d.ts +2 -0
- package/dist/__tests__/scanners/logic/index.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/logic/index.test.js +165 -0
- package/dist/__tests__/scanners/logic/index.test.js.map +1 -0
- package/dist/__tests__/scanners/logic/types.test.d.ts +2 -0
- package/dist/__tests__/scanners/logic/types.test.d.ts.map +1 -0
- package/dist/__tests__/scanners/logic/types.test.js +85 -0
- package/dist/__tests__/scanners/logic/types.test.js.map +1 -0
- package/dist/action/pr-comment.test.js +4 -0
- package/dist/action/pr-comment.test.js.map +1 -1
- package/dist/action/sarif-upload.test.js +4 -0
- package/dist/action/sarif-upload.test.js.map +1 -1
- package/dist/autofix/branch-manager.d.ts +115 -0
- package/dist/autofix/branch-manager.d.ts.map +1 -0
- package/dist/autofix/branch-manager.js +308 -0
- package/dist/autofix/branch-manager.js.map +1 -0
- package/dist/autofix/commit-generator.d.ts +55 -0
- package/dist/autofix/commit-generator.d.ts.map +1 -0
- package/dist/autofix/commit-generator.js +277 -0
- package/dist/autofix/commit-generator.js.map +1 -0
- package/dist/autofix/constitution.d.ts +77 -0
- package/dist/autofix/constitution.d.ts.map +1 -0
- package/dist/autofix/constitution.js +261 -0
- package/dist/autofix/constitution.js.map +1 -0
- package/dist/autofix/constitution.schema.d.ts +441 -0
- package/dist/autofix/constitution.schema.d.ts.map +1 -0
- package/dist/autofix/constitution.schema.js +144 -0
- package/dist/autofix/constitution.schema.js.map +1 -0
- package/dist/autofix/index.d.ts +13 -0
- package/dist/autofix/index.d.ts.map +1 -0
- package/dist/autofix/index.js +15 -0
- package/dist/autofix/index.js.map +1 -0
- package/dist/autofix/pr-generator.d.ts +57 -0
- package/dist/autofix/pr-generator.d.ts.map +1 -0
- package/dist/autofix/pr-generator.js +597 -0
- package/dist/autofix/pr-generator.js.map +1 -0
- package/dist/autofix/types.d.ts +151 -0
- package/dist/autofix/types.d.ts.map +1 -0
- package/dist/autofix/types.js +22 -0
- package/dist/autofix/types.js.map +1 -0
- package/dist/eval/fixtures.d.ts +20 -0
- package/dist/eval/fixtures.d.ts.map +1 -1
- package/dist/eval/fixtures.js +430 -0
- package/dist/eval/fixtures.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -1
- package/dist/index.js.map +1 -1
- package/dist/scanners/cache.d.ts.map +1 -1
- package/dist/scanners/cache.js +4 -0
- package/dist/scanners/cache.js.map +1 -1
- package/dist/scanners/dast/index.d.ts +39 -0
- package/dist/scanners/dast/index.d.ts.map +1 -0
- package/dist/scanners/dast/index.js +259 -0
- package/dist/scanners/dast/index.js.map +1 -0
- package/dist/scanners/dast/nuclei.d.ts +26 -0
- package/dist/scanners/dast/nuclei.d.ts.map +1 -0
- package/dist/scanners/dast/nuclei.js +354 -0
- package/dist/scanners/dast/nuclei.js.map +1 -0
- package/dist/scanners/dast/types.d.ts +306 -0
- package/dist/scanners/dast/types.d.ts.map +1 -0
- package/dist/scanners/dast/types.js +52 -0
- package/dist/scanners/dast/types.js.map +1 -0
- package/dist/scanners/dast/zap.d.ts +26 -0
- package/dist/scanners/dast/zap.d.ts.map +1 -0
- package/dist/scanners/dast/zap.js +453 -0
- package/dist/scanners/dast/zap.js.map +1 -0
- package/dist/scanners/fp-feedback.d.ts +140 -0
- package/dist/scanners/fp-feedback.d.ts.map +1 -0
- package/dist/scanners/fp-feedback.js +292 -0
- package/dist/scanners/fp-feedback.js.map +1 -0
- package/dist/scanners/fp-filter.d.ts +94 -0
- package/dist/scanners/fp-filter.d.ts.map +1 -0
- package/dist/scanners/fp-filter.js +397 -0
- package/dist/scanners/fp-filter.js.map +1 -0
- package/dist/scanners/fp-tracker.d.ts +125 -0
- package/dist/scanners/fp-tracker.d.ts.map +1 -0
- package/dist/scanners/fp-tracker.js +330 -0
- package/dist/scanners/fp-tracker.js.map +1 -0
- package/dist/scanners/index.d.ts.map +1 -1
- package/dist/scanners/index.js +56 -0
- package/dist/scanners/index.js.map +1 -1
- package/dist/scanners/index.test.js +6 -6
- package/dist/scanners/index.test.js.map +1 -1
- package/dist/scanners/logic/auth-flow-analyzer.d.ts +18 -0
- package/dist/scanners/logic/auth-flow-analyzer.d.ts.map +1 -0
- package/dist/scanners/logic/auth-flow-analyzer.js +384 -0
- package/dist/scanners/logic/auth-flow-analyzer.js.map +1 -0
- package/dist/scanners/logic/endpoint-analyzer.d.ts +29 -0
- package/dist/scanners/logic/endpoint-analyzer.d.ts.map +1 -0
- package/dist/scanners/logic/endpoint-analyzer.js +528 -0
- package/dist/scanners/logic/endpoint-analyzer.js.map +1 -0
- package/dist/scanners/logic/index.d.ts +41 -0
- package/dist/scanners/logic/index.d.ts.map +1 -0
- package/dist/scanners/logic/index.js +268 -0
- package/dist/scanners/logic/index.js.map +1 -0
- package/dist/scanners/logic/types.d.ts +254 -0
- package/dist/scanners/logic/types.d.ts.map +1 -0
- package/dist/scanners/logic/types.js +142 -0
- package/dist/scanners/logic/types.js.map +1 -0
- package/dist/scanners/types.d.ts +1 -1
- package/dist/scanners/types.d.ts.map +1 -1
- package/dist/scanners/types.js +4 -0
- package/dist/scanners/types.js.map +1 -1
- package/dist/telemetry/usage.d.ts +1 -1
- package/dist/telemetry/usage.d.ts.map +1 -1
- package/dist/telemetry/usage.js +14 -6
- package/dist/telemetry/usage.js.map +1 -1
- package/package.json +6 -8
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR Generator
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the creation of autofix pull requests.
|
|
5
|
+
* Uses gh CLI for GitHub operations (works locally and in CI).
|
|
6
|
+
*
|
|
7
|
+
* @module autofix/pr-generator
|
|
8
|
+
*/
|
|
9
|
+
import type { Finding, Severity } from "../certification/types.js";
|
|
10
|
+
import type { AutofixPRConfig, AutofixPRResult, AutofixGroup, BatchPRResult } from "./types.js";
|
|
11
|
+
import { type Constitution } from "./constitution.js";
|
|
12
|
+
/**
|
|
13
|
+
* Check if gh CLI is available
|
|
14
|
+
*/
|
|
15
|
+
export declare function isGhCliAvailable(): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Check if authenticated with gh CLI
|
|
18
|
+
*/
|
|
19
|
+
export declare function isGhAuthenticated(): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Group findings into PR groups based on config
|
|
22
|
+
*/
|
|
23
|
+
export declare function groupFindings(findings: Finding[], groupBy: AutofixPRConfig["groupBy"], branchPrefix: string): AutofixGroup[];
|
|
24
|
+
/**
|
|
25
|
+
* Create a single autofix PR for a group of findings
|
|
26
|
+
*/
|
|
27
|
+
export declare function createAutofixPR(config: AutofixPRConfig, group: AutofixGroup, constitution?: Constitution): Promise<AutofixPRResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Create autofix PRs for all findings
|
|
30
|
+
*/
|
|
31
|
+
export declare function createAutofixPRs(config: Partial<AutofixPRConfig> & {
|
|
32
|
+
projectPath: string;
|
|
33
|
+
findings: Finding[];
|
|
34
|
+
}, constitution?: Constitution): Promise<BatchPRResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Preview autofix PRs without creating them
|
|
37
|
+
*/
|
|
38
|
+
export declare function previewAutofixPRs(projectPath: string, findings: Finding[], options?: {
|
|
39
|
+
groupBy?: AutofixPRConfig["groupBy"];
|
|
40
|
+
includeUnsafe?: boolean;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
groups: Array<{
|
|
43
|
+
groupId: string;
|
|
44
|
+
branchName: string;
|
|
45
|
+
findings: number;
|
|
46
|
+
severity?: Severity;
|
|
47
|
+
fixable: number;
|
|
48
|
+
unfixable: number;
|
|
49
|
+
estimatedChanges: Array<{
|
|
50
|
+
file: string;
|
|
51
|
+
findingId: string;
|
|
52
|
+
canAutoFix: boolean;
|
|
53
|
+
risk: string;
|
|
54
|
+
}>;
|
|
55
|
+
}>;
|
|
56
|
+
}>;
|
|
57
|
+
//# sourceMappingURL=pr-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pr-generator.d.ts","sourceRoot":"","sources":["../../src/autofix/pr-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAGnE,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,aAAa,EAEd,MAAM,YAAY,CAAC;AA4BpB,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,mBAAmB,CAAC;AAE3B;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAYzD;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAY1D;AAwED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,EACnC,YAAY,EAAE,MAAM,GACnB,YAAY,EAAE,CA2EhB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,YAAY,EACnB,YAAY,CAAC,EAAE,YAAY,GAC1B,OAAO,CAAC,eAAe,CAAC,CAwM1B;AAqED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,EAC/E,YAAY,CAAC,EAAE,YAAY,GAC1B,OAAO,CAAC,aAAa,CAAC,CAmLxB;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GACA,OAAO,CAAC;IACT,MAAM,EAAE,KAAK,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,EAAE,KAAK,CAAC;YACtB,IAAI,EAAE,MAAM,CAAC;YACb,SAAS,EAAE,MAAM,CAAC;YAClB,UAAU,EAAE,OAAO,CAAC;YACpB,IAAI,EAAE,MAAM,CAAC;SACd,CAAC,CAAC;KACJ,CAAC,CAAC;CACJ,CAAC,CA8DD"}
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR Generator
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the creation of autofix pull requests.
|
|
5
|
+
* Uses gh CLI for GitHub operations (works locally and in CI).
|
|
6
|
+
*
|
|
7
|
+
* @module autofix/pr-generator
|
|
8
|
+
*/
|
|
9
|
+
import spawn from "cross-spawn";
|
|
10
|
+
import { applyFix, previewFix, FIX_PATTERNS } from "../certification/autofix.js";
|
|
11
|
+
import { git, isGitRepo, getCurrentBranch, getDefaultBranch, createBranch, checkoutBranch, stageFiles, pushBranch, getRemoteUrl, parseGitHubRemote, generateBranchName, ensureCleanWorkingTree, restoreOriginalState, } from "./branch-manager.js";
|
|
12
|
+
import { generateCommitMessage, generateCommitBody, createCommit, generatePRTitle, generatePRBody, } from "./commit-generator.js";
|
|
13
|
+
import { logger } from "../logger.js";
|
|
14
|
+
import { loadConstitution, isPathAllowed, isPatternApproved, } from "./constitution.js";
|
|
15
|
+
/**
|
|
16
|
+
* Check if gh CLI is available
|
|
17
|
+
*/
|
|
18
|
+
export async function isGhCliAvailable() {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const child = spawn("gh", ["--version"], { timeout: 5000 });
|
|
21
|
+
child.on("close", (code) => {
|
|
22
|
+
resolve(code === 0);
|
|
23
|
+
});
|
|
24
|
+
child.on("error", () => {
|
|
25
|
+
resolve(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if authenticated with gh CLI
|
|
31
|
+
*/
|
|
32
|
+
export async function isGhAuthenticated() {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const child = spawn("gh", ["auth", "status"], { timeout: 10000 });
|
|
35
|
+
child.on("close", (code) => {
|
|
36
|
+
resolve(code === 0);
|
|
37
|
+
});
|
|
38
|
+
child.on("error", () => {
|
|
39
|
+
resolve(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a PR using gh CLI
|
|
45
|
+
*/
|
|
46
|
+
async function createPRWithGh(cwd, title, body, baseBranch, headBranch, labels = []) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const args = [
|
|
49
|
+
"pr",
|
|
50
|
+
"create",
|
|
51
|
+
"--title",
|
|
52
|
+
title,
|
|
53
|
+
"--body",
|
|
54
|
+
body,
|
|
55
|
+
"--base",
|
|
56
|
+
baseBranch,
|
|
57
|
+
"--head",
|
|
58
|
+
headBranch,
|
|
59
|
+
];
|
|
60
|
+
if (labels.length > 0) {
|
|
61
|
+
args.push("--label", labels.join(","));
|
|
62
|
+
}
|
|
63
|
+
logger.info("gh.create_pr", { title, baseBranch, headBranch });
|
|
64
|
+
const child = spawn("gh", args, {
|
|
65
|
+
cwd,
|
|
66
|
+
timeout: 60000,
|
|
67
|
+
});
|
|
68
|
+
let stdout = "";
|
|
69
|
+
let stderr = "";
|
|
70
|
+
child.stdout?.on("data", (data) => {
|
|
71
|
+
stdout += data.toString();
|
|
72
|
+
});
|
|
73
|
+
child.stderr?.on("data", (data) => {
|
|
74
|
+
stderr += data.toString();
|
|
75
|
+
});
|
|
76
|
+
child.on("close", (code) => {
|
|
77
|
+
if (code === 0) {
|
|
78
|
+
// stdout contains the PR URL
|
|
79
|
+
const prUrl = stdout.trim();
|
|
80
|
+
// Extract PR number from URL
|
|
81
|
+
const prNumberMatch = prUrl.match(/\/pull\/(\d+)/);
|
|
82
|
+
const prNumber = prNumberMatch ? parseInt(prNumberMatch[1], 10) : 0;
|
|
83
|
+
logger.info("gh.pr_created", { prNumber, prUrl });
|
|
84
|
+
resolve({ prNumber, prUrl });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.error("gh.pr_create_failed", { stderr, exitCode: code });
|
|
88
|
+
resolve({ error: stderr || "Failed to create PR" });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
child.on("error", (error) => {
|
|
92
|
+
logger.error("gh.spawn_error", { error: String(error) });
|
|
93
|
+
resolve({ error: String(error) });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Group findings into PR groups based on config
|
|
99
|
+
*/
|
|
100
|
+
export function groupFindings(findings, groupBy, branchPrefix) {
|
|
101
|
+
const groups = [];
|
|
102
|
+
switch (groupBy) {
|
|
103
|
+
case "severity": {
|
|
104
|
+
const severityOrder = ["critical", "high", "medium", "low", "info"];
|
|
105
|
+
for (const severity of severityOrder) {
|
|
106
|
+
const matching = findings.filter((f) => f.severity === severity);
|
|
107
|
+
if (matching.length > 0) {
|
|
108
|
+
groups.push({
|
|
109
|
+
groupId: `severity-${severity}`,
|
|
110
|
+
branchName: generateBranchName(branchPrefix, severity),
|
|
111
|
+
findings: matching,
|
|
112
|
+
severity,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "file": {
|
|
119
|
+
const byFile = new Map();
|
|
120
|
+
for (const finding of findings) {
|
|
121
|
+
if (finding.file) {
|
|
122
|
+
const existing = byFile.get(finding.file) || [];
|
|
123
|
+
existing.push(finding);
|
|
124
|
+
byFile.set(finding.file, existing);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const [file, fileFindings] of byFile) {
|
|
128
|
+
const shortFile = file.split("/").pop() || file;
|
|
129
|
+
groups.push({
|
|
130
|
+
groupId: `file-${file}`,
|
|
131
|
+
branchName: generateBranchName(branchPrefix, shortFile),
|
|
132
|
+
findings: fileFindings,
|
|
133
|
+
file,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "pattern": {
|
|
139
|
+
const byPattern = new Map();
|
|
140
|
+
for (const finding of findings) {
|
|
141
|
+
const patternId = finding.category || "other";
|
|
142
|
+
const existing = byPattern.get(patternId) || [];
|
|
143
|
+
existing.push(finding);
|
|
144
|
+
byPattern.set(patternId, existing);
|
|
145
|
+
}
|
|
146
|
+
for (const [patternId, patternFindings] of byPattern) {
|
|
147
|
+
groups.push({
|
|
148
|
+
groupId: `pattern-${patternId}`,
|
|
149
|
+
branchName: generateBranchName(branchPrefix, patternId),
|
|
150
|
+
findings: patternFindings,
|
|
151
|
+
patternId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "single": {
|
|
157
|
+
for (let i = 0; i < findings.length; i++) {
|
|
158
|
+
const finding = findings[i];
|
|
159
|
+
groups.push({
|
|
160
|
+
groupId: `single-${finding.id}`,
|
|
161
|
+
branchName: generateBranchName(branchPrefix, finding.id),
|
|
162
|
+
findings: [finding],
|
|
163
|
+
severity: finding.severity,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return groups;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Create a single autofix PR for a group of findings
|
|
173
|
+
*/
|
|
174
|
+
export async function createAutofixPR(config, group, constitution) {
|
|
175
|
+
const { projectPath, baseBranch, dryRun, signCommits, includeUnsafe, remoteName } = config;
|
|
176
|
+
// Store original state
|
|
177
|
+
const originalBranch = await getCurrentBranch(projectPath);
|
|
178
|
+
if (!originalBranch) {
|
|
179
|
+
return {
|
|
180
|
+
branch: group.branchName,
|
|
181
|
+
fixesApplied: [],
|
|
182
|
+
filesModified: [],
|
|
183
|
+
dryRun,
|
|
184
|
+
error: "Could not determine current branch",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Ensure clean working tree
|
|
188
|
+
const { clean, stashed } = await ensureCleanWorkingTree(projectPath);
|
|
189
|
+
if (!clean) {
|
|
190
|
+
return {
|
|
191
|
+
branch: group.branchName,
|
|
192
|
+
fixesApplied: [],
|
|
193
|
+
filesModified: [],
|
|
194
|
+
dryRun,
|
|
195
|
+
error: "Working tree has uncommitted changes. Please commit or stash them first.",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
// Create and checkout the new branch
|
|
200
|
+
if (!dryRun) {
|
|
201
|
+
const branchResult = await createBranch(projectPath, group.branchName, baseBranch);
|
|
202
|
+
if (!branchResult.success) {
|
|
203
|
+
await restoreOriginalState(projectPath, originalBranch, stashed);
|
|
204
|
+
return {
|
|
205
|
+
branch: group.branchName,
|
|
206
|
+
fixesApplied: [],
|
|
207
|
+
filesModified: [],
|
|
208
|
+
dryRun,
|
|
209
|
+
error: `Failed to create branch: ${branchResult.stderr}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Apply fixes
|
|
214
|
+
const fixResults = [];
|
|
215
|
+
const filesModified = new Set();
|
|
216
|
+
for (const finding of group.findings) {
|
|
217
|
+
// Preview first
|
|
218
|
+
const preview = await previewFix(projectPath, finding);
|
|
219
|
+
if (!preview.canAutoFix) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (preview.risk === "high" && !includeUnsafe) {
|
|
223
|
+
logger.info("autofix.skipped_unsafe", { findingId: finding.id });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Apply the fix
|
|
227
|
+
const result = await applyFix(projectPath, finding, dryRun);
|
|
228
|
+
fixResults.push(result);
|
|
229
|
+
if (result.applied || result.diff) {
|
|
230
|
+
filesModified.add(result.file);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (fixResults.length === 0) {
|
|
234
|
+
if (!dryRun) {
|
|
235
|
+
await restoreOriginalState(projectPath, originalBranch, stashed);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
branch: group.branchName,
|
|
239
|
+
fixesApplied: [],
|
|
240
|
+
filesModified: [],
|
|
241
|
+
dryRun,
|
|
242
|
+
error: "No fixes could be applied",
|
|
243
|
+
severity: group.severity,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (dryRun) {
|
|
247
|
+
return {
|
|
248
|
+
branch: group.branchName,
|
|
249
|
+
fixesApplied: fixResults,
|
|
250
|
+
filesModified: Array.from(filesModified),
|
|
251
|
+
dryRun: true,
|
|
252
|
+
severity: group.severity,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// Stage the modified files
|
|
256
|
+
const stageResult = await stageFiles(projectPath, Array.from(filesModified));
|
|
257
|
+
if (!stageResult.success) {
|
|
258
|
+
await restoreOriginalState(projectPath, originalBranch, stashed);
|
|
259
|
+
return {
|
|
260
|
+
branch: group.branchName,
|
|
261
|
+
fixesApplied: fixResults,
|
|
262
|
+
filesModified: Array.from(filesModified),
|
|
263
|
+
dryRun,
|
|
264
|
+
error: `Failed to stage files: ${stageResult.stderr}`,
|
|
265
|
+
severity: group.severity,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// Create commit
|
|
269
|
+
const commitMessage = generateCommitMessage(fixResults, group.severity);
|
|
270
|
+
const commitBody = generateCommitBody(fixResults, group.severity);
|
|
271
|
+
const commitResult = await createCommit(projectPath, commitMessage, commitBody);
|
|
272
|
+
if ("error" in commitResult) {
|
|
273
|
+
await restoreOriginalState(projectPath, originalBranch, stashed);
|
|
274
|
+
return {
|
|
275
|
+
branch: group.branchName,
|
|
276
|
+
fixesApplied: fixResults,
|
|
277
|
+
filesModified: Array.from(filesModified),
|
|
278
|
+
dryRun,
|
|
279
|
+
error: commitResult.error,
|
|
280
|
+
severity: group.severity,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// Push branch
|
|
284
|
+
const pushResult = await pushBranch(projectPath, group.branchName, remoteName);
|
|
285
|
+
if (!pushResult.success) {
|
|
286
|
+
await restoreOriginalState(projectPath, originalBranch, stashed);
|
|
287
|
+
return {
|
|
288
|
+
branch: group.branchName,
|
|
289
|
+
fixesApplied: fixResults,
|
|
290
|
+
filesModified: Array.from(filesModified),
|
|
291
|
+
commitSha: commitResult.sha,
|
|
292
|
+
dryRun,
|
|
293
|
+
error: `Failed to push branch: ${pushResult.stderr}`,
|
|
294
|
+
severity: group.severity,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Create PR with constitution labels
|
|
298
|
+
const prLabels = constitution?.prRules.requiredLabels || ["autofix", "security"];
|
|
299
|
+
const prTitle = generatePRTitle(fixResults, group.severity, config.titleTemplate);
|
|
300
|
+
const prBody = generatePRBody(fixResults, group.severity, {
|
|
301
|
+
includeBeforeAfter: true,
|
|
302
|
+
});
|
|
303
|
+
const prResult = await createPRWithGh(projectPath, prTitle, prBody, baseBranch, group.branchName, prLabels);
|
|
304
|
+
// Restore original branch
|
|
305
|
+
await checkoutBranch(projectPath, originalBranch);
|
|
306
|
+
if (stashed) {
|
|
307
|
+
await git(["stash", "pop"], { cwd: projectPath });
|
|
308
|
+
}
|
|
309
|
+
if ("error" in prResult) {
|
|
310
|
+
return {
|
|
311
|
+
branch: group.branchName,
|
|
312
|
+
fixesApplied: fixResults,
|
|
313
|
+
filesModified: Array.from(filesModified),
|
|
314
|
+
commitSha: commitResult.sha,
|
|
315
|
+
dryRun,
|
|
316
|
+
error: prResult.error,
|
|
317
|
+
severity: group.severity,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
prNumber: prResult.prNumber,
|
|
322
|
+
prUrl: prResult.prUrl,
|
|
323
|
+
branch: group.branchName,
|
|
324
|
+
fixesApplied: fixResults,
|
|
325
|
+
filesModified: Array.from(filesModified),
|
|
326
|
+
commitSha: commitResult.sha,
|
|
327
|
+
dryRun,
|
|
328
|
+
severity: group.severity,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
// Always try to restore original state on error
|
|
333
|
+
try {
|
|
334
|
+
await restoreOriginalState(projectPath, originalBranch, stashed);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Ignore restoration errors
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
branch: group.branchName,
|
|
341
|
+
fixesApplied: [],
|
|
342
|
+
filesModified: [],
|
|
343
|
+
dryRun,
|
|
344
|
+
error: String(error),
|
|
345
|
+
severity: group.severity,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Find the fix pattern for a finding
|
|
351
|
+
*/
|
|
352
|
+
function findPatternForFinding(finding) {
|
|
353
|
+
// Try matching by category first
|
|
354
|
+
if (finding.category) {
|
|
355
|
+
const categoryStr = typeof finding.category === "string" ? finding.category : String(finding.category);
|
|
356
|
+
const pattern = FIX_PATTERNS.find((p) => p.patternId === categoryStr || p.patternId.includes(categoryStr));
|
|
357
|
+
if (pattern)
|
|
358
|
+
return pattern;
|
|
359
|
+
}
|
|
360
|
+
// Try matching by scanner source
|
|
361
|
+
if (finding.scanner_source) {
|
|
362
|
+
const pattern = FIX_PATTERNS.find((p) => p.patternId.includes(finding.scanner_source || ""));
|
|
363
|
+
if (pattern)
|
|
364
|
+
return pattern;
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Filter findings based on constitution rules
|
|
370
|
+
*/
|
|
371
|
+
function filterFindingsByConstitution(findings, constitution) {
|
|
372
|
+
const allowed = [];
|
|
373
|
+
const blocked = [];
|
|
374
|
+
for (const finding of findings) {
|
|
375
|
+
// Check path restrictions
|
|
376
|
+
if (finding.file) {
|
|
377
|
+
const pathCheck = isPathAllowed(constitution, finding.file);
|
|
378
|
+
if (!pathCheck.allowed) {
|
|
379
|
+
blocked.push({ finding, reason: pathCheck.reason || "Path not allowed" });
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Check pattern approval
|
|
384
|
+
const pattern = findPatternForFinding(finding);
|
|
385
|
+
if (pattern) {
|
|
386
|
+
const severity = (finding.severity || "medium");
|
|
387
|
+
const approval = isPatternApproved(constitution, pattern, {
|
|
388
|
+
filePath: finding.file || "",
|
|
389
|
+
severity,
|
|
390
|
+
});
|
|
391
|
+
if (!approval.approved) {
|
|
392
|
+
blocked.push({
|
|
393
|
+
finding,
|
|
394
|
+
reason: approval.reason || "Pattern not approved",
|
|
395
|
+
});
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
allowed.push(finding);
|
|
400
|
+
}
|
|
401
|
+
return { allowed, blocked };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Create autofix PRs for all findings
|
|
405
|
+
*/
|
|
406
|
+
export async function createAutofixPRs(config, constitution) {
|
|
407
|
+
// Load constitution if not provided
|
|
408
|
+
const effectiveConstitution = constitution || await loadConstitution(config.projectPath);
|
|
409
|
+
const fullConfig = {
|
|
410
|
+
branchPrefix: "vaspera/autofix",
|
|
411
|
+
baseBranch: "main",
|
|
412
|
+
// Use constitution's groupBy if specified
|
|
413
|
+
groupBy: effectiveConstitution.prRules.groupBy,
|
|
414
|
+
// Use constitution's dryRunDefault
|
|
415
|
+
dryRun: effectiveConstitution.safety.dryRunDefault,
|
|
416
|
+
signCommits: false,
|
|
417
|
+
// Use aggressive risk tolerance to include unsafe
|
|
418
|
+
includeUnsafe: effectiveConstitution.riskTolerance === "aggressive",
|
|
419
|
+
remoteName: "origin",
|
|
420
|
+
...config,
|
|
421
|
+
};
|
|
422
|
+
const { projectPath, findings, groupBy, dryRun } = fullConfig;
|
|
423
|
+
// Filter findings based on constitution
|
|
424
|
+
const { allowed: allowedFindings, blocked } = filterFindingsByConstitution(findings, effectiveConstitution);
|
|
425
|
+
logger.info("constitution.filtered_findings", {
|
|
426
|
+
total: findings.length,
|
|
427
|
+
allowed: allowedFindings.length,
|
|
428
|
+
blocked: blocked.length,
|
|
429
|
+
});
|
|
430
|
+
// Add blocked findings to unfixable list
|
|
431
|
+
const unfixable = blocked.map(({ finding, reason }) => ({ findingId: finding.id, reason }));
|
|
432
|
+
// Check if any findings remain
|
|
433
|
+
if (allowedFindings.length === 0) {
|
|
434
|
+
return {
|
|
435
|
+
totalFindings: findings.length,
|
|
436
|
+
prsCreated: [],
|
|
437
|
+
unfixable,
|
|
438
|
+
success: unfixable.length === 0,
|
|
439
|
+
summary: { bySeverity: {}, byFile: {}, byPattern: {} },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
// Validate prerequisites
|
|
443
|
+
if (!(await isGitRepo(projectPath))) {
|
|
444
|
+
return {
|
|
445
|
+
totalFindings: findings.length,
|
|
446
|
+
prsCreated: [],
|
|
447
|
+
unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "Not a git repository" }))],
|
|
448
|
+
success: false,
|
|
449
|
+
summary: { bySeverity: {}, byFile: {}, byPattern: {} },
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (!dryRun) {
|
|
453
|
+
const ghAvailable = await isGhCliAvailable();
|
|
454
|
+
if (!ghAvailable) {
|
|
455
|
+
return {
|
|
456
|
+
totalFindings: findings.length,
|
|
457
|
+
prsCreated: [],
|
|
458
|
+
unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "gh CLI not available" }))],
|
|
459
|
+
success: false,
|
|
460
|
+
summary: { bySeverity: {}, byFile: {}, byPattern: {} },
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const ghAuthenticated = await isGhAuthenticated();
|
|
464
|
+
if (!ghAuthenticated) {
|
|
465
|
+
return {
|
|
466
|
+
totalFindings: findings.length,
|
|
467
|
+
prsCreated: [],
|
|
468
|
+
unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "gh CLI not authenticated" }))],
|
|
469
|
+
success: false,
|
|
470
|
+
summary: { bySeverity: {}, byFile: {}, byPattern: {} },
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const remoteUrl = await getRemoteUrl(projectPath, fullConfig.remoteName);
|
|
474
|
+
if (!remoteUrl || !parseGitHubRemote(remoteUrl)) {
|
|
475
|
+
return {
|
|
476
|
+
totalFindings: findings.length,
|
|
477
|
+
prsCreated: [],
|
|
478
|
+
unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "Not a GitHub repository" }))],
|
|
479
|
+
success: false,
|
|
480
|
+
summary: { bySeverity: {}, byFile: {}, byPattern: {} },
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Get default branch if not specified
|
|
485
|
+
if (fullConfig.baseBranch === "main") {
|
|
486
|
+
fullConfig.baseBranch = await getDefaultBranch(projectPath);
|
|
487
|
+
}
|
|
488
|
+
// Check max files limit from constitution
|
|
489
|
+
const maxFiles = effectiveConstitution.safety.maxFilesPerRun;
|
|
490
|
+
const uniqueFiles = new Set(allowedFindings.map((f) => f.file).filter(Boolean));
|
|
491
|
+
if (uniqueFiles.size > maxFiles) {
|
|
492
|
+
logger.warn("constitution.max_files_exceeded", {
|
|
493
|
+
files: uniqueFiles.size,
|
|
494
|
+
maxFiles,
|
|
495
|
+
});
|
|
496
|
+
// Continue but log warning - could also return error here
|
|
497
|
+
}
|
|
498
|
+
// Check max PRs limit from constitution
|
|
499
|
+
const maxPRs = effectiveConstitution.prRules.maxPRsPerRun;
|
|
500
|
+
// Group findings
|
|
501
|
+
let groups = groupFindings(allowedFindings, groupBy, fullConfig.branchPrefix);
|
|
502
|
+
// Limit to maxPRsPerRun
|
|
503
|
+
if (groups.length > maxPRs) {
|
|
504
|
+
logger.warn("constitution.max_prs_exceeded", {
|
|
505
|
+
groups: groups.length,
|
|
506
|
+
maxPRs,
|
|
507
|
+
});
|
|
508
|
+
groups = groups.slice(0, maxPRs);
|
|
509
|
+
}
|
|
510
|
+
logger.info("autofix.creating_prs", {
|
|
511
|
+
totalFindings: findings.length,
|
|
512
|
+
allowedFindings: allowedFindings.length,
|
|
513
|
+
groups: groups.length,
|
|
514
|
+
groupBy,
|
|
515
|
+
dryRun,
|
|
516
|
+
});
|
|
517
|
+
// Create PRs for each group, passing constitution for labels
|
|
518
|
+
const prsCreated = [];
|
|
519
|
+
for (const group of groups) {
|
|
520
|
+
const result = await createAutofixPR(fullConfig, group, effectiveConstitution);
|
|
521
|
+
prsCreated.push(result);
|
|
522
|
+
if (result.error) {
|
|
523
|
+
for (const finding of group.findings) {
|
|
524
|
+
unfixable.push({ findingId: finding.id, reason: result.error });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Build summary
|
|
529
|
+
const summary = {
|
|
530
|
+
bySeverity: {},
|
|
531
|
+
byFile: {},
|
|
532
|
+
byPattern: {},
|
|
533
|
+
};
|
|
534
|
+
for (const pr of prsCreated) {
|
|
535
|
+
if (pr.severity) {
|
|
536
|
+
summary.bySeverity[pr.severity] = (summary.bySeverity[pr.severity] || 0) + pr.fixesApplied.length;
|
|
537
|
+
}
|
|
538
|
+
for (const file of pr.filesModified) {
|
|
539
|
+
summary.byFile[file] = (summary.byFile[file] || 0) + 1;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const allSuccessful = prsCreated.every((pr) => !pr.error);
|
|
543
|
+
logger.info("autofix.batch_complete", {
|
|
544
|
+
prsCreated: prsCreated.length,
|
|
545
|
+
successfulPrs: prsCreated.filter((pr) => !pr.error).length,
|
|
546
|
+
totalFixes: prsCreated.reduce((sum, pr) => sum + pr.fixesApplied.length, 0),
|
|
547
|
+
unfixable: unfixable.length,
|
|
548
|
+
});
|
|
549
|
+
return {
|
|
550
|
+
totalFindings: findings.length,
|
|
551
|
+
prsCreated,
|
|
552
|
+
unfixable,
|
|
553
|
+
success: allSuccessful,
|
|
554
|
+
summary,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Preview autofix PRs without creating them
|
|
559
|
+
*/
|
|
560
|
+
export async function previewAutofixPRs(projectPath, findings, options) {
|
|
561
|
+
const groupBy = options?.groupBy ?? "severity";
|
|
562
|
+
const includeUnsafe = options?.includeUnsafe ?? false;
|
|
563
|
+
const groups = groupFindings(findings, groupBy, "vaspera/autofix");
|
|
564
|
+
const result = [];
|
|
565
|
+
for (const group of groups) {
|
|
566
|
+
const changes = [];
|
|
567
|
+
let fixable = 0;
|
|
568
|
+
let unfixable = 0;
|
|
569
|
+
for (const finding of group.findings) {
|
|
570
|
+
const preview = await previewFix(projectPath, finding);
|
|
571
|
+
const canFix = preview.canAutoFix && (includeUnsafe || preview.risk !== "high");
|
|
572
|
+
changes.push({
|
|
573
|
+
file: finding.file || "unknown",
|
|
574
|
+
findingId: finding.id,
|
|
575
|
+
canAutoFix: canFix,
|
|
576
|
+
risk: preview.risk,
|
|
577
|
+
});
|
|
578
|
+
if (canFix) {
|
|
579
|
+
fixable++;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
unfixable++;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
result.push({
|
|
586
|
+
groupId: group.groupId,
|
|
587
|
+
branchName: group.branchName,
|
|
588
|
+
findings: group.findings.length,
|
|
589
|
+
severity: group.severity,
|
|
590
|
+
fixable,
|
|
591
|
+
unfixable,
|
|
592
|
+
estimatedChanges: changes,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return { groups: result };
|
|
596
|
+
}
|
|
597
|
+
//# sourceMappingURL=pr-generator.js.map
|