@qasshq/qass 0.1.2
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/LICENSE +40 -0
- package/README.md +163 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +117 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +128 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/diff-analyzer.d.ts +3 -0
- package/dist/core/diff-analyzer.d.ts.map +1 -0
- package/dist/core/diff-analyzer.js +194 -0
- package/dist/core/diff-analyzer.js.map +1 -0
- package/dist/core/discover.d.ts +3 -0
- package/dist/core/discover.d.ts.map +1 -0
- package/dist/core/discover.js +51 -0
- package/dist/core/discover.js.map +1 -0
- package/dist/core/license.d.ts +13 -0
- package/dist/core/license.d.ts.map +1 -0
- package/dist/core/license.js +132 -0
- package/dist/core/license.js.map +1 -0
- package/dist/core/report.d.ts +4 -0
- package/dist/core/report.d.ts.map +1 -0
- package/dist/core/report.js +95 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/runner.d.ts +3 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +136 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/test-planner.d.ts +3 -0
- package/dist/core/test-planner.d.ts.map +1 -0
- package/dist/core/test-planner.js +107 -0
- package/dist/core/test-planner.js.map +1 -0
- package/dist/integrations/cursor-rule.d.ts +2 -0
- package/dist/integrations/cursor-rule.d.ts.map +1 -0
- package/dist/integrations/cursor-rule.js +46 -0
- package/dist/integrations/cursor-rule.js.map +1 -0
- package/dist/integrations/mcp-server.d.ts +67 -0
- package/dist/integrations/mcp-server.d.ts.map +1 -0
- package/dist/integrations/mcp-server.js +61 -0
- package/dist/integrations/mcp-server.js.map +1 -0
- package/dist/runners/api/api-runner.d.ts +3 -0
- package/dist/runners/api/api-runner.d.ts.map +1 -0
- package/dist/runners/api/api-runner.js +258 -0
- package/dist/runners/api/api-runner.js.map +1 -0
- package/dist/runners/api/endpoint-discovery.d.ts +3 -0
- package/dist/runners/api/endpoint-discovery.d.ts.map +1 -0
- package/dist/runners/api/endpoint-discovery.js +106 -0
- package/dist/runners/api/endpoint-discovery.js.map +1 -0
- package/dist/runners/e2e/playwright-runner.d.ts +3 -0
- package/dist/runners/e2e/playwright-runner.d.ts.map +1 -0
- package/dist/runners/e2e/playwright-runner.js +309 -0
- package/dist/runners/e2e/playwright-runner.js.map +1 -0
- package/dist/runners/security/dynamic-checker.d.ts +3 -0
- package/dist/runners/security/dynamic-checker.d.ts.map +1 -0
- package/dist/runners/security/dynamic-checker.js +136 -0
- package/dist/runners/security/dynamic-checker.js.map +1 -0
- package/dist/runners/security/rules/auth-middleware.d.ts +13 -0
- package/dist/runners/security/rules/auth-middleware.d.ts.map +1 -0
- package/dist/runners/security/rules/auth-middleware.js +94 -0
- package/dist/runners/security/rules/auth-middleware.js.map +1 -0
- package/dist/runners/security/rules/config-audit.d.ts +14 -0
- package/dist/runners/security/rules/config-audit.d.ts.map +1 -0
- package/dist/runners/security/rules/config-audit.js +91 -0
- package/dist/runners/security/rules/config-audit.js.map +1 -0
- package/dist/runners/security/rules/dep-audit.d.ts +7 -0
- package/dist/runners/security/rules/dep-audit.d.ts.map +1 -0
- package/dist/runners/security/rules/dep-audit.js +82 -0
- package/dist/runners/security/rules/dep-audit.js.map +1 -0
- package/dist/runners/security/rules/input-sanitization.d.ts +12 -0
- package/dist/runners/security/rules/input-sanitization.d.ts.map +1 -0
- package/dist/runners/security/rules/input-sanitization.js +64 -0
- package/dist/runners/security/rules/input-sanitization.js.map +1 -0
- package/dist/runners/security/rules/rate-limit-audit.d.ts +11 -0
- package/dist/runners/security/rules/rate-limit-audit.d.ts.map +1 -0
- package/dist/runners/security/rules/rate-limit-audit.js +51 -0
- package/dist/runners/security/rules/rate-limit-audit.js.map +1 -0
- package/dist/runners/security/rules/secrets-scan.d.ts +4 -0
- package/dist/runners/security/rules/secrets-scan.d.ts.map +1 -0
- package/dist/runners/security/rules/secrets-scan.js +129 -0
- package/dist/runners/security/rules/secrets-scan.js.map +1 -0
- package/dist/runners/security/rules/xss-vectors.d.ts +13 -0
- package/dist/runners/security/rules/xss-vectors.d.ts.map +1 -0
- package/dist/runners/security/rules/xss-vectors.js +76 -0
- package/dist/runners/security/rules/xss-vectors.js.map +1 -0
- package/dist/runners/security/static-analyzer.d.ts +7 -0
- package/dist/runners/security/static-analyzer.d.ts.map +1 -0
- package/dist/runners/security/static-analyzer.js +87 -0
- package/dist/runners/security/static-analyzer.js.map +1 -0
- package/dist/runners/unit/unit-runner.d.ts +3 -0
- package/dist/runners/unit/unit-runner.d.ts.map +1 -0
- package/dist/runners/unit/unit-runner.js +157 -0
- package/dist/runners/unit/unit-runner.js.map +1 -0
- package/dist/types.d.ts +153 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util/glob.d.ts +2 -0
- package/dist/util/glob.d.ts.map +1 -0
- package/dist/util/glob.js +32 -0
- package/dist/util/glob.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export async function runE2ETests(config, projectPath, diff, smokeOnly = false) {
|
|
3
|
+
const results = [];
|
|
4
|
+
let playwright;
|
|
5
|
+
try {
|
|
6
|
+
const mod = "playwright";
|
|
7
|
+
playwright = await import(/* @vite-ignore */ mod);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
console.log(chalk.yellow(" Playwright not installed. Run: npm i -D playwright"));
|
|
11
|
+
return results;
|
|
12
|
+
}
|
|
13
|
+
const frontendUrl = config.services?.frontend?.url;
|
|
14
|
+
if (!frontendUrl) {
|
|
15
|
+
console.log(chalk.dim(" No frontend service configured"));
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
18
|
+
const isUp = await checkFrontend(frontendUrl);
|
|
19
|
+
if (!isUp) {
|
|
20
|
+
console.log(chalk.yellow(" Frontend not running. Skipping E2E tests."));
|
|
21
|
+
return results;
|
|
22
|
+
}
|
|
23
|
+
const changedPages = diff.changedFiles
|
|
24
|
+
.filter((f) => f.category === "frontend_page" || f.category === "component")
|
|
25
|
+
.map((f) => filePathToRoute(f.path));
|
|
26
|
+
const pagesToTest = changedPages.length > 0 ? changedPages : ["/"];
|
|
27
|
+
console.log(chalk.dim(` Testing ${pagesToTest.length} pages...`));
|
|
28
|
+
const browser = await playwright.chromium.launch({ headless: true });
|
|
29
|
+
try {
|
|
30
|
+
const viewports = config.e2e?.viewports ?? [
|
|
31
|
+
{ width: 1280, height: 720, name: "desktop" },
|
|
32
|
+
];
|
|
33
|
+
for (const page of pagesToTest) {
|
|
34
|
+
for (const viewport of viewports) {
|
|
35
|
+
const context = await browser.newContext({
|
|
36
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
37
|
+
});
|
|
38
|
+
const browserPage = await context.newPage();
|
|
39
|
+
const pageResults = await testPage(browserPage, frontendUrl, page, viewport.name, config, projectPath, smokeOnly);
|
|
40
|
+
results.push(...pageResults);
|
|
41
|
+
await context.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!smokeOnly && config.flows) {
|
|
45
|
+
const flowResults = await runFlows(playwright, browser, config, frontendUrl, projectPath);
|
|
46
|
+
results.push(...flowResults);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await browser.close();
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
async function testPage(page, baseUrl, route, viewportName, config, projectPath, smokeOnly) {
|
|
55
|
+
const results = [];
|
|
56
|
+
const url = `${baseUrl}${route}`;
|
|
57
|
+
const errors = [];
|
|
58
|
+
const networkErrors = [];
|
|
59
|
+
page.on("console", (msg) => {
|
|
60
|
+
if (msg.type() === "error") {
|
|
61
|
+
errors.push(msg.text());
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
page.on("pageerror", (err) => {
|
|
65
|
+
errors.push(`Uncaught: ${err.message}`);
|
|
66
|
+
});
|
|
67
|
+
page.on("response", (res) => {
|
|
68
|
+
if (res.status() >= 400) {
|
|
69
|
+
networkErrors.push(`${res.status()} ${res.url()}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
page.on("requestfailed", (req) => {
|
|
73
|
+
networkErrors.push(`FAILED ${req.url()} ${req.failure()?.errorText ?? ""}`);
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
const response = await page.goto(url, {
|
|
77
|
+
waitUntil: "networkidle",
|
|
78
|
+
timeout: 15000,
|
|
79
|
+
});
|
|
80
|
+
if (!response || response.status() >= 400) {
|
|
81
|
+
results.push({
|
|
82
|
+
name: `Page load ${route} [${viewportName}]`,
|
|
83
|
+
type: "e2e",
|
|
84
|
+
status: "failed",
|
|
85
|
+
error: `Page returned ${response?.status() ?? "no response"}`,
|
|
86
|
+
fix: `Check that ${route} is a valid route and the frontend is configured correctly.`,
|
|
87
|
+
});
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
results.push({
|
|
91
|
+
name: `Page load ${route} [${viewportName}]`,
|
|
92
|
+
type: "e2e",
|
|
93
|
+
status: "passed",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
results.push({
|
|
98
|
+
name: `Page load ${route} [${viewportName}]`,
|
|
99
|
+
type: "e2e",
|
|
100
|
+
status: "failed",
|
|
101
|
+
error: `Navigation failed: ${e instanceof Error ? e.message : "timeout"}`,
|
|
102
|
+
fix: `Page may be crashing on load. Check for initialization errors in the component.`,
|
|
103
|
+
});
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
if (errors.length > 0) {
|
|
107
|
+
results.push({
|
|
108
|
+
name: `Console errors on ${route} [${viewportName}]`,
|
|
109
|
+
type: "e2e",
|
|
110
|
+
status: "failed",
|
|
111
|
+
error: `${errors.length} console error(s): ${errors.slice(0, 3).join("; ")}`,
|
|
112
|
+
fix: `Fix the console errors on ${route}. These indicate runtime JavaScript failures.`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (networkErrors.length > 0) {
|
|
116
|
+
const critical = networkErrors.filter((e) => e.startsWith("5") || e.startsWith("FAILED"));
|
|
117
|
+
if (critical.length > 0) {
|
|
118
|
+
results.push({
|
|
119
|
+
name: `Network errors on ${route} [${viewportName}]`,
|
|
120
|
+
type: "e2e",
|
|
121
|
+
status: "failed",
|
|
122
|
+
error: `${critical.length} failed request(s): ${critical.slice(0, 3).join("; ")}`,
|
|
123
|
+
fix: `Fix the failed API calls on ${route}. 5xx errors indicate server-side issues.`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (config.e2e?.smoke_crawl !== false) {
|
|
128
|
+
const smokeResults = await smokeCrawl(page, route, viewportName, config);
|
|
129
|
+
results.push(...smokeResults);
|
|
130
|
+
}
|
|
131
|
+
if (!smokeOnly && config.e2e?.visual_regression) {
|
|
132
|
+
const vizResult = await visualRegression(page, route, viewportName, projectPath, config);
|
|
133
|
+
if (vizResult)
|
|
134
|
+
results.push(vizResult);
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
async function smokeCrawl(page, route, viewportName, config) {
|
|
139
|
+
const results = [];
|
|
140
|
+
try {
|
|
141
|
+
const buttons = await page.$$("button:visible, [role='button']:visible");
|
|
142
|
+
let clickErrors = 0;
|
|
143
|
+
for (const btn of buttons.slice(0, 10)) {
|
|
144
|
+
try {
|
|
145
|
+
const text = await btn.textContent();
|
|
146
|
+
if (/sign out|logout|delete|remove|cancel/i.test(text ?? ""))
|
|
147
|
+
continue;
|
|
148
|
+
const errorsBeforeClick = [];
|
|
149
|
+
const errorHandler = (msg) => {
|
|
150
|
+
if (msg.type() === "error")
|
|
151
|
+
errorsBeforeClick.push(msg.text());
|
|
152
|
+
};
|
|
153
|
+
page.on("console", errorHandler);
|
|
154
|
+
await btn.click({ timeout: 3000 }).catch(() => { });
|
|
155
|
+
await page.waitForTimeout(500);
|
|
156
|
+
page.removeListener("console", errorHandler);
|
|
157
|
+
if (errorsBeforeClick.length > 0)
|
|
158
|
+
clickErrors++;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// element may have been removed
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (clickErrors > 0) {
|
|
165
|
+
results.push({
|
|
166
|
+
name: `Smoke crawl ${route} [${viewportName}]: button clicks`,
|
|
167
|
+
type: "e2e",
|
|
168
|
+
status: "failed",
|
|
169
|
+
error: `${clickErrors} button(s) caused console errors when clicked`,
|
|
170
|
+
fix: `Test buttons on ${route} — some are throwing errors on click.`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const stuckSpinners = await page.$$("[class*='spinner']:visible, [class*='loading']:visible, [class*='skeleton']:visible");
|
|
174
|
+
const timeout = config.e2e?.stuck_timeout ?? 10000;
|
|
175
|
+
if (stuckSpinners.length > 0) {
|
|
176
|
+
await page.waitForTimeout(timeout);
|
|
177
|
+
const stillSpinning = await page.$$("[class*='spinner']:visible, [class*='loading']:visible, [class*='skeleton']:visible");
|
|
178
|
+
if (stillSpinning.length > 0) {
|
|
179
|
+
results.push({
|
|
180
|
+
name: `Smoke crawl ${route} [${viewportName}]: stuck loading`,
|
|
181
|
+
type: "e2e",
|
|
182
|
+
status: "failed",
|
|
183
|
+
error: `${stillSpinning.length} loading indicator(s) still visible after ${timeout / 1000}s`,
|
|
184
|
+
fix: `Something on ${route} is stuck loading. Check for failed API calls or infinite loading states.`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// smoke crawl failed gracefully
|
|
191
|
+
}
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
async function visualRegression(page, route, viewportName, projectPath, _config) {
|
|
195
|
+
const { resolve, join } = await import("node:path");
|
|
196
|
+
const { mkdir, readFile, writeFile } = await import("node:fs/promises");
|
|
197
|
+
const baselineDir = resolve(projectPath, ".qass", "baselines");
|
|
198
|
+
const resultDir = resolve(projectPath, ".qass", "results", "screenshots");
|
|
199
|
+
await mkdir(baselineDir, { recursive: true });
|
|
200
|
+
await mkdir(resultDir, { recursive: true });
|
|
201
|
+
const safeName = route.replace(/[^a-zA-Z0-9]/g, "_");
|
|
202
|
+
const baselinePath = join(baselineDir, `${safeName}_${viewportName}.png`);
|
|
203
|
+
const currentPath = join(resultDir, `${safeName}_${viewportName}.png`);
|
|
204
|
+
const screenshot = await page.screenshot({ fullPage: true });
|
|
205
|
+
await writeFile(currentPath, screenshot);
|
|
206
|
+
try {
|
|
207
|
+
const baseline = await readFile(baselinePath);
|
|
208
|
+
if (!Buffer.from(screenshot).equals(baseline)) {
|
|
209
|
+
return {
|
|
210
|
+
name: `Visual regression ${route} [${viewportName}]`,
|
|
211
|
+
type: "e2e",
|
|
212
|
+
status: "failed",
|
|
213
|
+
screenshot: currentPath,
|
|
214
|
+
error: `Screenshot differs from baseline.`,
|
|
215
|
+
fix: `Visual change detected on ${route}. Check ${currentPath} against baseline. Run 'qass test' with --update-baselines to accept the new version.`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
await writeFile(baselinePath, screenshot);
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
async function runFlows(playwright, browser, config, baseUrl, _projectPath) {
|
|
225
|
+
const results = [];
|
|
226
|
+
for (const [name, flow] of Object.entries(config.flows ?? {})) {
|
|
227
|
+
console.log(chalk.dim(` Flow: ${name}`));
|
|
228
|
+
const context = await browser.newContext();
|
|
229
|
+
const page = await context.newPage();
|
|
230
|
+
let passed = true;
|
|
231
|
+
let error = "";
|
|
232
|
+
for (const step of flow.steps) {
|
|
233
|
+
try {
|
|
234
|
+
if ("goto" in step) {
|
|
235
|
+
await page.goto(`${baseUrl}${step.goto}`, {
|
|
236
|
+
waitUntil: "networkidle",
|
|
237
|
+
timeout: 15000,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else if ("fill" in step) {
|
|
241
|
+
const [selector, value] = step.fill.split(" = ");
|
|
242
|
+
await page.fill(selector.trim(), value?.trim() ?? "");
|
|
243
|
+
}
|
|
244
|
+
else if ("click" in step) {
|
|
245
|
+
await page.click(step.click, { timeout: 5000 });
|
|
246
|
+
}
|
|
247
|
+
else if ("wait" in step) {
|
|
248
|
+
await page.waitForSelector(step.wait, { timeout: 10000 });
|
|
249
|
+
}
|
|
250
|
+
else if ("wait_url" in step) {
|
|
251
|
+
await page.waitForURL(`**${step.wait_url}*`, { timeout: 10000 });
|
|
252
|
+
}
|
|
253
|
+
else if ("assert_visible" in step) {
|
|
254
|
+
const el = await page.$(step.assert_visible);
|
|
255
|
+
if (!el || !(await el.isVisible())) {
|
|
256
|
+
throw new Error(`Element not visible: ${step.assert_visible}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if ("assert_hidden" in step) {
|
|
260
|
+
const el = await page.$(step.assert_hidden);
|
|
261
|
+
if (el && (await el.isVisible())) {
|
|
262
|
+
throw new Error(`Element should be hidden: ${step.assert_hidden}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if ("assert_url" in step) {
|
|
266
|
+
const currentUrl = page.url();
|
|
267
|
+
if (!currentUrl.includes(step.assert_url)) {
|
|
268
|
+
throw new Error(`URL mismatch: expected ${step.assert_url}, got ${currentUrl}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
passed = false;
|
|
274
|
+
error = `Step failed: ${JSON.stringify(step)} — ${e instanceof Error ? e.message : "unknown"}`;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
results.push({
|
|
279
|
+
name: `Flow: ${name}`,
|
|
280
|
+
type: "e2e",
|
|
281
|
+
status: passed ? "passed" : "failed",
|
|
282
|
+
error: error || undefined,
|
|
283
|
+
fix: error
|
|
284
|
+
? `Fix the failing step in the '${name}' flow. The flow definition is in .qass/config.yaml.`
|
|
285
|
+
: undefined,
|
|
286
|
+
});
|
|
287
|
+
await context.close();
|
|
288
|
+
}
|
|
289
|
+
return results;
|
|
290
|
+
}
|
|
291
|
+
async function checkFrontend(url) {
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
294
|
+
return res.ok || res.status < 500;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function filePathToRoute(filePath) {
|
|
301
|
+
let route = filePath
|
|
302
|
+
.replace(/.*app\//, "/")
|
|
303
|
+
.replace(/\/page\.(tsx|jsx|ts|js)$/, "")
|
|
304
|
+
.replace(/\[([^\]]+)\]/g, "test-$1");
|
|
305
|
+
if (!route || route === "/")
|
|
306
|
+
route = "/";
|
|
307
|
+
return route;
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=playwright-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright-runner.js","sourceRoot":"","sources":["../../../src/runners/e2e/playwright-runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-checker.d.ts","sourceRoot":"","sources":["../../../src/runners/security/dynamic-checker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAY,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG5E,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,EAAE,CAAC,CAqB5B"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { discoverEndpoints } from "../api/endpoint-discovery.js";
|
|
3
|
+
export async function runDynamicSecurityChecks(config, projectPath) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
const baseUrl = getApiBaseUrl(config);
|
|
6
|
+
if (!baseUrl)
|
|
7
|
+
return findings;
|
|
8
|
+
const isUp = await isServerUp(baseUrl, config);
|
|
9
|
+
if (!isUp) {
|
|
10
|
+
console.log(chalk.dim(" API not running, skipping dynamic checks"));
|
|
11
|
+
return findings;
|
|
12
|
+
}
|
|
13
|
+
const endpoints = await discoverEndpoints(config, projectPath);
|
|
14
|
+
console.log(chalk.dim(` Checking ${endpoints.length} endpoints...`));
|
|
15
|
+
findings.push(...(await checkUnauthenticatedAccess(endpoints, baseUrl)));
|
|
16
|
+
findings.push(...(await checkSecurityHeaders(baseUrl, config)));
|
|
17
|
+
findings.push(...(await checkErrorDisclosure(endpoints, baseUrl)));
|
|
18
|
+
return findings;
|
|
19
|
+
}
|
|
20
|
+
async function checkUnauthenticatedAccess(endpoints, baseUrl) {
|
|
21
|
+
const findings = [];
|
|
22
|
+
const authEndpoints = endpoints.filter((ep) => ep.requiresAuth && ep.method === "GET");
|
|
23
|
+
for (const ep of authEndpoints) {
|
|
24
|
+
const url = `${baseUrl}${ep.fullPath.replace(/:(\w+)/g, "test-$1")}`;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: "GET",
|
|
28
|
+
signal: AbortSignal.timeout(5000),
|
|
29
|
+
});
|
|
30
|
+
if (res.status === 200) {
|
|
31
|
+
findings.push({
|
|
32
|
+
rule: "dynamic-unauth-access",
|
|
33
|
+
severity: "HIGH",
|
|
34
|
+
file: ep.routeFile,
|
|
35
|
+
description: `${ep.method} ${ep.fullPath} returns 200 without authentication. This endpoint should require auth but is publicly accessible.`,
|
|
36
|
+
fix: `Add auth middleware to this route: router.${ep.method.toLowerCase()}("${ep.path}", auth, ...)`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (res.status >= 500) {
|
|
40
|
+
findings.push({
|
|
41
|
+
rule: "dynamic-unauth-access",
|
|
42
|
+
severity: "MEDIUM",
|
|
43
|
+
file: ep.routeFile,
|
|
44
|
+
description: `${ep.method} ${ep.fullPath} returns ${res.status} without authentication. The handler may be crashing instead of returning 401.`,
|
|
45
|
+
fix: `Ensure auth middleware runs before the handler. The 500 error suggests the handler tries to access req.user before auth validates the token.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// timeout or connection error
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
async function checkSecurityHeaders(baseUrl, config) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
const requiredHeaders = config.security?.required_headers ?? [
|
|
58
|
+
"X-Content-Type-Options",
|
|
59
|
+
"Strict-Transport-Security",
|
|
60
|
+
"X-Frame-Options",
|
|
61
|
+
];
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(baseUrl, {
|
|
64
|
+
method: "GET",
|
|
65
|
+
signal: AbortSignal.timeout(5000),
|
|
66
|
+
});
|
|
67
|
+
for (const header of requiredHeaders) {
|
|
68
|
+
if (!res.headers.get(header)) {
|
|
69
|
+
findings.push({
|
|
70
|
+
rule: "dynamic-security-headers",
|
|
71
|
+
severity: "LOW",
|
|
72
|
+
file: "API configuration",
|
|
73
|
+
description: `Missing security header: ${header}. This header helps protect against common web attacks.`,
|
|
74
|
+
fix: header === "Strict-Transport-Security"
|
|
75
|
+
? "Add Helmet middleware or set the header manually: app.use(helmet())"
|
|
76
|
+
: `Ensure Helmet is configured and the ${header} header is not disabled.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// server not reachable
|
|
83
|
+
}
|
|
84
|
+
return findings;
|
|
85
|
+
}
|
|
86
|
+
async function checkErrorDisclosure(endpoints, baseUrl) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
const testEndpoints = endpoints.filter((ep) => ep.method === "GET").slice(0, 5);
|
|
89
|
+
for (const ep of testEndpoints) {
|
|
90
|
+
const url = `${baseUrl}${ep.fullPath.replace(/:(\w+)/g, "test-$1")}`;
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(url, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: "{{invalid json",
|
|
96
|
+
signal: AbortSignal.timeout(5000),
|
|
97
|
+
});
|
|
98
|
+
if (res.status >= 400) {
|
|
99
|
+
const body = await res.text();
|
|
100
|
+
const leaksInfo = /stack|trace|at\s+\w+\s+\(|node_modules|\.ts:|\.js:/i.test(body) ||
|
|
101
|
+
/ECONNREFUSED|ENOTFOUND|password|secret/i.test(body);
|
|
102
|
+
if (leaksInfo) {
|
|
103
|
+
findings.push({
|
|
104
|
+
rule: "dynamic-error-disclosure",
|
|
105
|
+
severity: "MEDIUM",
|
|
106
|
+
file: ep.routeFile,
|
|
107
|
+
description: `Error response from ${ep.method} ${ep.fullPath} leaks internal details (stack traces, file paths, or sensitive information).`,
|
|
108
|
+
fix: `Ensure the error handler returns generic messages in production: res.json({ error: "Internal server error" }) instead of exposing err.message or err.stack.`,
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// expected
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
function getApiBaseUrl(config) {
|
|
121
|
+
const api = config.services?.api;
|
|
122
|
+
if (!api)
|
|
123
|
+
return null;
|
|
124
|
+
return api.health?.replace(/\/[^/]*$/, "") ?? `http://localhost:${api.port}`;
|
|
125
|
+
}
|
|
126
|
+
async function isServerUp(baseUrl, config) {
|
|
127
|
+
const healthUrl = config.services?.api?.health;
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch(healthUrl ?? baseUrl, { signal: AbortSignal.timeout(5000) });
|
|
130
|
+
return res.ok || res.status < 500;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=dynamic-checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-checker.js","sourceRoot":"","sources":["../../../src/runners/security/dynamic-checker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { QassConfig, SecurityFinding } from "../../../types.js";
|
|
2
|
+
import type { FileContent } from "../static-analyzer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Detects route handlers that access req.user without auth middleware in the chain.
|
|
5
|
+
*
|
|
6
|
+
* Checks for:
|
|
7
|
+
* - router.get/post/put/delete/patch calls where the handler uses req.user
|
|
8
|
+
* but 'auth' is not in the middleware arguments
|
|
9
|
+
* - Routes that should have auth based on accessing user-specific data
|
|
10
|
+
* but have no auth middleware applied (either inline or via router.use)
|
|
11
|
+
*/
|
|
12
|
+
export declare function runAuthMiddlewareRule(files: FileContent[], _config: QassConfig): Promise<SecurityFinding[]>;
|
|
13
|
+
//# sourceMappingURL=auth-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-middleware.d.ts","sourceRoot":"","sources":["../../../../src/runners/security/rules/auth-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,eAAe,EAAE,CAAC,CAqE5B"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects route handlers that access req.user without auth middleware in the chain.
|
|
3
|
+
*
|
|
4
|
+
* Checks for:
|
|
5
|
+
* - router.get/post/put/delete/patch calls where the handler uses req.user
|
|
6
|
+
* but 'auth' is not in the middleware arguments
|
|
7
|
+
* - Routes that should have auth based on accessing user-specific data
|
|
8
|
+
* but have no auth middleware applied (either inline or via router.use)
|
|
9
|
+
*/
|
|
10
|
+
export async function runAuthMiddlewareRule(files, _config) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
const routeFiles = files.filter((f) => f.path.endsWith(".routes.ts") || f.path.endsWith(".routes.js"));
|
|
13
|
+
for (const file of routeFiles) {
|
|
14
|
+
const lines = file.content.split("\n");
|
|
15
|
+
const hasRouterLevelAuth = checkRouterLevelAuth(file.content);
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
const routeMatch = line.match(/router\.(get|post|put|delete|patch)\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
19
|
+
if (!routeMatch)
|
|
20
|
+
continue;
|
|
21
|
+
const method = routeMatch[1].toUpperCase();
|
|
22
|
+
const path = routeMatch[2];
|
|
23
|
+
const middlewareArgs = extractMiddlewareArgs(lines, i);
|
|
24
|
+
const handlerBody = extractHandlerBody(lines, i);
|
|
25
|
+
const usesReqUser = /req\.user/.test(handlerBody) ||
|
|
26
|
+
/req\.fleetCourier/.test(handlerBody) ||
|
|
27
|
+
/req\.fleetContext/.test(handlerBody);
|
|
28
|
+
const hasInlineAuth = middlewareArgs.includes("auth") ||
|
|
29
|
+
middlewareArgs.includes("requireAuth");
|
|
30
|
+
if (usesReqUser && !hasInlineAuth && !hasRouterLevelAuth) {
|
|
31
|
+
findings.push({
|
|
32
|
+
rule: "auth-middleware",
|
|
33
|
+
severity: "HIGH",
|
|
34
|
+
file: file.path,
|
|
35
|
+
line: i + 1,
|
|
36
|
+
description: `${method} ${path} accesses req.user but 'auth' middleware is not in the middleware chain. Unauthenticated requests will reach this handler with req.user undefined.`,
|
|
37
|
+
fix: `Add auth middleware to the route definition. Change to: router.${method.toLowerCase()}("${path}", auth, ${middlewareArgs.length > 0 ? middlewareArgs.join(", ") + ", " : ""}async (req, res) => {`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const accessesSensitiveData = /\.select\(/.test(handlerBody) &&
|
|
41
|
+
(/email|password|phone|token|secret|key/i.test(handlerBody));
|
|
42
|
+
if (accessesSensitiveData &&
|
|
43
|
+
!hasInlineAuth &&
|
|
44
|
+
!hasRouterLevelAuth &&
|
|
45
|
+
!usesReqUser) {
|
|
46
|
+
findings.push({
|
|
47
|
+
rule: "auth-middleware",
|
|
48
|
+
severity: "MEDIUM",
|
|
49
|
+
file: file.path,
|
|
50
|
+
line: i + 1,
|
|
51
|
+
description: `${method} ${path} accesses sensitive data but has no auth middleware. This endpoint may be publicly accessible.`,
|
|
52
|
+
fix: `Add auth middleware: router.${method.toLowerCase()}("${path}", auth, ...)`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
function checkRouterLevelAuth(content) {
|
|
60
|
+
return /router\.use\s*\(\s*auth\b/.test(content);
|
|
61
|
+
}
|
|
62
|
+
function extractMiddlewareArgs(lines, startLine) {
|
|
63
|
+
const chunk = lines.slice(startLine, startLine + 5).join(" ");
|
|
64
|
+
const argsMatch = chunk.match(/router\.\w+\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(.*?)\s*(?:async\s*)?\(/);
|
|
65
|
+
if (!argsMatch)
|
|
66
|
+
return [];
|
|
67
|
+
return argsMatch[1]
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((a) => a.trim())
|
|
70
|
+
.filter((a) => a && !a.startsWith("async") && !a.startsWith("("));
|
|
71
|
+
}
|
|
72
|
+
function extractHandlerBody(lines, startLine) {
|
|
73
|
+
let braceDepth = 0;
|
|
74
|
+
let started = false;
|
|
75
|
+
const bodyLines = [];
|
|
76
|
+
for (let i = startLine; i < Math.min(startLine + 150, lines.length); i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
for (const ch of line) {
|
|
79
|
+
if (ch === "{") {
|
|
80
|
+
braceDepth++;
|
|
81
|
+
started = true;
|
|
82
|
+
}
|
|
83
|
+
else if (ch === "}") {
|
|
84
|
+
braceDepth--;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (started)
|
|
88
|
+
bodyLines.push(line);
|
|
89
|
+
if (started && braceDepth <= 0)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return bodyLines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=auth-middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-middleware.js","sourceRoot":"","sources":["../../../../src/runners/security/rules/auth-middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { QassConfig, SecurityFinding } from "../../../types.js";
|
|
2
|
+
import type { FileContent } from "../static-analyzer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Audits application configuration for security issues.
|
|
5
|
+
*
|
|
6
|
+
* Checks for:
|
|
7
|
+
* - CORS with localhost origins in non-dev configuration
|
|
8
|
+
* - Helmet CSP disabled
|
|
9
|
+
* - Error handlers leaking err.message in production
|
|
10
|
+
* - trust proxy misconfiguration
|
|
11
|
+
* - Hardcoded admin emails as fallbacks
|
|
12
|
+
*/
|
|
13
|
+
export declare function runConfigAuditRule(files: FileContent[], _config: QassConfig): Promise<SecurityFinding[]>;
|
|
14
|
+
//# sourceMappingURL=config-audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-audit.d.ts","sourceRoot":"","sources":["../../../../src/runners/security/rules/config-audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,eAAe,EAAE,CAAC,CAgG5B"}
|