@mainahq/core 0.2.0 → 0.4.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/package.json +4 -1
- package/src/ai/__tests__/delegation.test.ts +105 -0
- package/src/ai/delegation.ts +111 -0
- package/src/ai/try-generate.ts +17 -0
- package/src/cloud/__tests__/auth.test.ts +164 -0
- package/src/cloud/__tests__/client.test.ts +253 -0
- package/src/cloud/auth.ts +232 -0
- package/src/cloud/client.ts +190 -0
- package/src/cloud/types.ts +106 -0
- package/src/context/relevance.ts +5 -0
- package/src/context/retrieval.ts +3 -0
- package/src/context/semantic.ts +3 -0
- package/src/feedback/__tests__/trace-analysis.test.ts +98 -0
- package/src/feedback/trace-analysis.ts +153 -0
- package/src/index.ts +55 -0
- package/src/init/__tests__/init.test.ts +51 -0
- package/src/init/index.ts +43 -0
- package/src/language/__tests__/detect.test.ts +61 -1
- package/src/language/__tests__/profile.test.ts +68 -1
- package/src/language/detect.ts +33 -3
- package/src/language/profile.ts +67 -2
- package/src/ticket/index.ts +5 -0
- package/src/verify/__tests__/consistency.test.ts +98 -0
- package/src/verify/__tests__/lighthouse.test.ts +215 -0
- package/src/verify/__tests__/linters/checkstyle.test.ts +23 -0
- package/src/verify/__tests__/linters/dotnet-format.test.ts +18 -0
- package/src/verify/__tests__/pipeline.test.ts +21 -2
- package/src/verify/__tests__/typecheck.test.ts +160 -0
- package/src/verify/__tests__/zap.test.ts +188 -0
- package/src/verify/consistency.ts +199 -0
- package/src/verify/detect.ts +13 -1
- package/src/verify/lighthouse.ts +173 -0
- package/src/verify/linters/checkstyle.ts +41 -0
- package/src/verify/linters/dotnet-format.ts +37 -0
- package/src/verify/pipeline.ts +20 -2
- package/src/verify/syntax-guard.ts +8 -0
- package/src/verify/typecheck.ts +178 -0
- package/src/verify/zap.ts +189 -0
package/src/verify/pipeline.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { detectLanguages } from "../language/detect";
|
|
|
19
19
|
import type { LanguageId } from "../language/profile";
|
|
20
20
|
import { getProfile } from "../language/profile";
|
|
21
21
|
import { type AIReviewResult, runAIReview } from "./ai-review";
|
|
22
|
+
import { checkConsistency } from "./consistency";
|
|
22
23
|
import { runCoverage } from "./coverage";
|
|
23
24
|
import type { DetectedTool } from "./detect";
|
|
24
25
|
import { detectTools } from "./detect";
|
|
@@ -32,6 +33,7 @@ import { runSonar } from "./sonar";
|
|
|
32
33
|
import type { SyntaxDiagnostic } from "./syntax-guard";
|
|
33
34
|
import { syntaxGuard } from "./syntax-guard";
|
|
34
35
|
import { runTrivy } from "./trivy";
|
|
36
|
+
import { runTypecheck } from "./typecheck";
|
|
35
37
|
|
|
36
38
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
37
39
|
|
|
@@ -223,10 +225,26 @@ export async function runPipeline(
|
|
|
223
225
|
),
|
|
224
226
|
);
|
|
225
227
|
|
|
228
|
+
// Built-in checks (always run, no external tool dependency)
|
|
229
|
+
toolPromises.push(
|
|
230
|
+
runToolWithTiming("typecheck", async () => {
|
|
231
|
+
const result = await runTypecheck(files, cwd, { language: primaryLang });
|
|
232
|
+
return { findings: result.findings, skipped: result.skipped };
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
toolPromises.push(
|
|
237
|
+
runToolWithTiming("consistency", async () => {
|
|
238
|
+
const result = await checkConsistency(files, cwd, mainaDir);
|
|
239
|
+
return { findings: result.findings, skipped: false };
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
226
243
|
const toolReports = await Promise.all(toolPromises);
|
|
227
244
|
|
|
228
245
|
// ── Step 4b: Warn if all external tools were skipped ─────────────────
|
|
229
|
-
const
|
|
246
|
+
const builtInTools = new Set(["slop", "typecheck", "consistency"]);
|
|
247
|
+
const externalTools = toolReports.filter((r) => !builtInTools.has(r.tool));
|
|
230
248
|
const allExternalSkipped =
|
|
231
249
|
externalTools.length > 0 && externalTools.every((r) => r.skipped);
|
|
232
250
|
|
|
@@ -242,7 +260,7 @@ export async function runPipeline(
|
|
|
242
260
|
tool: "pipeline",
|
|
243
261
|
file: "",
|
|
244
262
|
line: 0,
|
|
245
|
-
message: `No external verification tools
|
|
263
|
+
message: `WARNING: No external verification tools detected (${skippedNames} skipped). Built-in checks (typecheck, consistency, slop) still ran. Run \`maina init --install\` to add external tools.`,
|
|
246
264
|
severity: "warning",
|
|
247
265
|
});
|
|
248
266
|
}
|
|
@@ -13,7 +13,9 @@ import { join } from "node:path";
|
|
|
13
13
|
import type { Result } from "../db/index";
|
|
14
14
|
import type { LanguageProfile } from "../language/profile";
|
|
15
15
|
import { TYPESCRIPT_PROFILE } from "../language/profile";
|
|
16
|
+
import { parseCheckstyleOutput } from "./linters/checkstyle";
|
|
16
17
|
import { parseClippyOutput } from "./linters/clippy";
|
|
18
|
+
import { parseDotnetFormatOutput } from "./linters/dotnet-format";
|
|
17
19
|
import { parseGoVetOutput } from "./linters/go-vet";
|
|
18
20
|
import { parseRuffOutput } from "./linters/ruff";
|
|
19
21
|
|
|
@@ -225,6 +227,12 @@ async function runLanguageLinter(
|
|
|
225
227
|
case "rust":
|
|
226
228
|
diagnostics = parseClippyOutput(stdout);
|
|
227
229
|
break;
|
|
230
|
+
case "csharp":
|
|
231
|
+
diagnostics = parseDotnetFormatOutput(stdout);
|
|
232
|
+
break;
|
|
233
|
+
case "java":
|
|
234
|
+
diagnostics = parseCheckstyleOutput(stdout);
|
|
235
|
+
break;
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Type Checking — runs language-native type checkers as a verify step.
|
|
3
|
+
*
|
|
4
|
+
* Zero external tool install required for TypeScript projects (uses project's tsc).
|
|
5
|
+
* For other languages: mypy (Python), go vet (Go), cargo check (Rust),
|
|
6
|
+
* dotnet build (C#), javac (Java).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import type { LanguageId } from "../language/profile";
|
|
12
|
+
import type { Finding } from "./diff-filter";
|
|
13
|
+
|
|
14
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface TypecheckResult {
|
|
17
|
+
findings: Finding[];
|
|
18
|
+
duration: number;
|
|
19
|
+
tool: string;
|
|
20
|
+
skipped: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TypecheckCommand {
|
|
24
|
+
tool: string;
|
|
25
|
+
command: string;
|
|
26
|
+
args: string[];
|
|
27
|
+
configFile?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Language-specific commands ───────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const TYPECHECK_COMMANDS: Record<LanguageId, TypecheckCommand> = {
|
|
33
|
+
typescript: {
|
|
34
|
+
tool: "tsc",
|
|
35
|
+
command: "tsc",
|
|
36
|
+
args: ["--noEmit", "--pretty", "false"],
|
|
37
|
+
configFile: "tsconfig.json",
|
|
38
|
+
},
|
|
39
|
+
python: {
|
|
40
|
+
tool: "mypy",
|
|
41
|
+
command: "mypy",
|
|
42
|
+
args: ["--no-color-output", "--no-error-summary"],
|
|
43
|
+
},
|
|
44
|
+
go: {
|
|
45
|
+
tool: "go-vet",
|
|
46
|
+
command: "go",
|
|
47
|
+
args: ["vet", "./..."],
|
|
48
|
+
},
|
|
49
|
+
rust: {
|
|
50
|
+
tool: "cargo-check",
|
|
51
|
+
command: "cargo",
|
|
52
|
+
args: ["check", "--message-format=short"],
|
|
53
|
+
},
|
|
54
|
+
csharp: {
|
|
55
|
+
tool: "dotnet-build",
|
|
56
|
+
command: "dotnet",
|
|
57
|
+
args: ["build", "--no-restore", "--verbosity", "quiet"],
|
|
58
|
+
},
|
|
59
|
+
java: {
|
|
60
|
+
tool: "javac",
|
|
61
|
+
command: "javac",
|
|
62
|
+
args: ["-Xlint:all"],
|
|
63
|
+
},
|
|
64
|
+
php: {
|
|
65
|
+
tool: "phpstan",
|
|
66
|
+
command: "phpstan",
|
|
67
|
+
args: ["analyse", "--error-format=json", "--no-progress"],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function getTypecheckCommand(language: LanguageId): TypecheckCommand {
|
|
72
|
+
return TYPECHECK_COMMANDS[language];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── TSC Output Parser ───────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse tsc --noEmit --pretty false output into Finding[].
|
|
79
|
+
*
|
|
80
|
+
* Format: file(line,col): error TSxxxx: message
|
|
81
|
+
*/
|
|
82
|
+
export function parseTscOutput(output: string): Finding[] {
|
|
83
|
+
if (!output.trim()) return [];
|
|
84
|
+
|
|
85
|
+
const findings: Finding[] = [];
|
|
86
|
+
const pattern =
|
|
87
|
+
/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm;
|
|
88
|
+
|
|
89
|
+
for (const match of output.matchAll(pattern)) {
|
|
90
|
+
findings.push({
|
|
91
|
+
tool: "tsc",
|
|
92
|
+
file: match[1] ?? "",
|
|
93
|
+
line: Number.parseInt(match[2] ?? "0", 10),
|
|
94
|
+
column: Number.parseInt(match[3] ?? "0", 10),
|
|
95
|
+
message: `${match[5] ?? ""}: ${match[6] ?? ""}`,
|
|
96
|
+
severity: match[4] === "error" ? "error" : "warning",
|
|
97
|
+
ruleId: match[5] ?? "",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return findings;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Runner ──────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export async function runTypecheck(
|
|
107
|
+
files: string[],
|
|
108
|
+
cwd: string,
|
|
109
|
+
options?: { command?: string; language?: LanguageId },
|
|
110
|
+
): Promise<TypecheckResult> {
|
|
111
|
+
const language = options?.language ?? "typescript";
|
|
112
|
+
const cmd = TYPECHECK_COMMANDS[language];
|
|
113
|
+
const start = performance.now();
|
|
114
|
+
|
|
115
|
+
// Check config file exists (e.g., tsconfig.json for TS)
|
|
116
|
+
if (cmd.configFile && !existsSync(join(cwd, cmd.configFile))) {
|
|
117
|
+
return {
|
|
118
|
+
findings: [],
|
|
119
|
+
duration: performance.now() - start,
|
|
120
|
+
tool: cmd.tool,
|
|
121
|
+
skipped: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Resolve command: check node_modules/.bin first (for tsc, mypy, etc.)
|
|
126
|
+
const localBin = join(cwd, "node_modules", ".bin", cmd.command);
|
|
127
|
+
const command =
|
|
128
|
+
options?.command ?? (existsSync(localBin) ? localBin : cmd.command);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const proc = Bun.spawn([command, ...cmd.args], {
|
|
132
|
+
cwd,
|
|
133
|
+
stdout: "pipe",
|
|
134
|
+
stderr: "pipe",
|
|
135
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const stdout = await new Response(proc.stdout).text();
|
|
139
|
+
const stderr = await new Response(proc.stderr).text();
|
|
140
|
+
await proc.exited;
|
|
141
|
+
|
|
142
|
+
const output = stdout + stderr;
|
|
143
|
+
let findings: Finding[];
|
|
144
|
+
|
|
145
|
+
if (language === "typescript") {
|
|
146
|
+
findings = parseTscOutput(output);
|
|
147
|
+
} else {
|
|
148
|
+
// For other languages, treat any non-zero exit as a generic finding
|
|
149
|
+
findings =
|
|
150
|
+
proc.exitCode !== 0 && output.trim()
|
|
151
|
+
? [
|
|
152
|
+
{
|
|
153
|
+
tool: cmd.tool,
|
|
154
|
+
file: files[0] ?? "unknown",
|
|
155
|
+
line: 1,
|
|
156
|
+
message: output.trim().split("\n")[0] ?? "Type check failed",
|
|
157
|
+
severity: "error" as const,
|
|
158
|
+
},
|
|
159
|
+
]
|
|
160
|
+
: [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
findings,
|
|
165
|
+
duration: performance.now() - start,
|
|
166
|
+
tool: cmd.tool,
|
|
167
|
+
skipped: false,
|
|
168
|
+
};
|
|
169
|
+
} catch {
|
|
170
|
+
// Command not found or other spawn error
|
|
171
|
+
return {
|
|
172
|
+
findings: [],
|
|
173
|
+
duration: performance.now() - start,
|
|
174
|
+
tool: cmd.tool,
|
|
175
|
+
skipped: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZAP DAST Integration for the Verify Engine.
|
|
3
|
+
*
|
|
4
|
+
* Runs OWASP ZAP baseline scan via Docker against a target URL.
|
|
5
|
+
* Parses JSON output into the unified Finding type.
|
|
6
|
+
* Gracefully skips if Docker is not available or no target URL configured.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isToolAvailable } from "./detect";
|
|
10
|
+
import type { Finding } from "./diff-filter";
|
|
11
|
+
|
|
12
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface ZapOptions {
|
|
15
|
+
targetUrl: string;
|
|
16
|
+
cwd: string;
|
|
17
|
+
/** Pre-resolved availability — skips redundant detection if provided. */
|
|
18
|
+
available?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ZapResult {
|
|
22
|
+
findings: Finding[];
|
|
23
|
+
skipped: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── JSON Parsing ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map ZAP risk description to unified severity.
|
|
30
|
+
*
|
|
31
|
+
* ZAP riskdesc format: "High (Medium)", "Medium (Low)", "Low (Medium)", etc.
|
|
32
|
+
* We extract the first word (risk level) to map severity.
|
|
33
|
+
*/
|
|
34
|
+
function mapZapRisk(riskdesc: string): "error" | "warning" | "info" {
|
|
35
|
+
const risk = riskdesc.split(" ")[0]?.toLowerCase() ?? "";
|
|
36
|
+
switch (risk) {
|
|
37
|
+
case "high":
|
|
38
|
+
return "error";
|
|
39
|
+
case "medium":
|
|
40
|
+
return "warning";
|
|
41
|
+
case "low":
|
|
42
|
+
case "informational":
|
|
43
|
+
return "info";
|
|
44
|
+
default:
|
|
45
|
+
return "warning";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse ZAP JSON output into Finding[].
|
|
51
|
+
*
|
|
52
|
+
* ZAP JSON baseline report has this structure:
|
|
53
|
+
* ```json
|
|
54
|
+
* {
|
|
55
|
+
* "site": [{
|
|
56
|
+
* "alerts": [{
|
|
57
|
+
* "pluginid": "10021",
|
|
58
|
+
* "alert": "X-Content-Type-Options Header Missing",
|
|
59
|
+
* "riskdesc": "Low (Medium)",
|
|
60
|
+
* "desc": "The Anti-MIME-Sniffing header is not set.",
|
|
61
|
+
* "instances": [{
|
|
62
|
+
* "uri": "https://example.com/api/health",
|
|
63
|
+
* "method": "GET"
|
|
64
|
+
* }]
|
|
65
|
+
* }]
|
|
66
|
+
* }]
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* Each alert may have multiple instances (URLs). We create one Finding per
|
|
71
|
+
* instance. If an alert has no instances, we still emit one Finding with
|
|
72
|
+
* an empty file.
|
|
73
|
+
*
|
|
74
|
+
* Handles malformed JSON and unexpected structures gracefully.
|
|
75
|
+
*/
|
|
76
|
+
export function parseZapJson(json: string): Finding[] {
|
|
77
|
+
let parsed: Record<string, unknown>;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(json) as Record<string, unknown>;
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sites = parsed.site;
|
|
85
|
+
if (!Array.isArray(sites)) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const findings: Finding[] = [];
|
|
90
|
+
|
|
91
|
+
for (const site of sites) {
|
|
92
|
+
const s = site as Record<string, unknown>;
|
|
93
|
+
const alerts = s.alerts;
|
|
94
|
+
|
|
95
|
+
if (!Array.isArray(alerts)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const alert of alerts) {
|
|
100
|
+
const a = alert as Record<string, unknown>;
|
|
101
|
+
const pluginId = (a.pluginid as string) ?? undefined;
|
|
102
|
+
const alertName = (a.alert as string) ?? "";
|
|
103
|
+
const riskdesc = (a.riskdesc as string) ?? "Informational";
|
|
104
|
+
const desc = (a.desc as string) ?? "";
|
|
105
|
+
const instances = a.instances;
|
|
106
|
+
const severity = mapZapRisk(riskdesc);
|
|
107
|
+
|
|
108
|
+
const message = `${alertName}: ${desc}`;
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(instances) && instances.length > 0) {
|
|
111
|
+
for (const instance of instances) {
|
|
112
|
+
const inst = instance as Record<string, unknown>;
|
|
113
|
+
const uri = (inst.uri as string) ?? "";
|
|
114
|
+
|
|
115
|
+
findings.push({
|
|
116
|
+
tool: "zap",
|
|
117
|
+
file: uri,
|
|
118
|
+
line: 0,
|
|
119
|
+
message,
|
|
120
|
+
severity,
|
|
121
|
+
ruleId: pluginId,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
findings.push({
|
|
126
|
+
tool: "zap",
|
|
127
|
+
file: "",
|
|
128
|
+
line: 0,
|
|
129
|
+
message,
|
|
130
|
+
severity,
|
|
131
|
+
ruleId: pluginId,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return findings;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Runner ───────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run ZAP baseline scan via Docker and return parsed findings.
|
|
144
|
+
*
|
|
145
|
+
* If Docker is not available or no targetUrl is configured,
|
|
146
|
+
* returns `{ findings: [], skipped: true }`.
|
|
147
|
+
* If ZAP fails, returns `{ findings: [], skipped: false }`.
|
|
148
|
+
*/
|
|
149
|
+
export async function runZap(options: ZapOptions): Promise<ZapResult> {
|
|
150
|
+
if (!options.targetUrl) {
|
|
151
|
+
return { findings: [], skipped: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const toolAvailable = options.available ?? (await isToolAvailable("zap"));
|
|
155
|
+
if (!toolAvailable) {
|
|
156
|
+
return { findings: [], skipped: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cwd = options.cwd;
|
|
160
|
+
|
|
161
|
+
const args = [
|
|
162
|
+
"docker",
|
|
163
|
+
"run",
|
|
164
|
+
"--rm",
|
|
165
|
+
"zaproxy/zap-stable",
|
|
166
|
+
"zap-baseline.py",
|
|
167
|
+
"-t",
|
|
168
|
+
options.targetUrl,
|
|
169
|
+
"-J",
|
|
170
|
+
"report.json",
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const proc = Bun.spawn(args, {
|
|
175
|
+
cwd,
|
|
176
|
+
stdout: "pipe",
|
|
177
|
+
stderr: "pipe",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const stdout = await new Response(proc.stdout).text();
|
|
181
|
+
await new Response(proc.stderr).text();
|
|
182
|
+
await proc.exited;
|
|
183
|
+
|
|
184
|
+
const findings = parseZapJson(stdout);
|
|
185
|
+
return { findings, skipped: false };
|
|
186
|
+
} catch {
|
|
187
|
+
return { findings: [], skipped: false };
|
|
188
|
+
}
|
|
189
|
+
}
|