opensecurity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PR Comment Reporter
4
+ *
5
+ * Reads a scan-results.json file and outputs a Markdown summary
6
+ * suitable for posting as a GitHub PR comment.
7
+ *
8
+ * Usage: node dist/pr-comment.js <scan-results.json>
9
+ */
10
+ import fs from "node:fs";
11
+ const SEVERITY_EMOJI = {
12
+ critical: "πŸ”΄",
13
+ high: "🟠",
14
+ medium: "🟑",
15
+ low: "πŸ”΅"
16
+ };
17
+ const SEVERITY_ORDER = ["critical", "high", "medium", "low"];
18
+ const MAX_FINDINGS_PER_SEVERITY = 10;
19
+ function loadResults(filePath) {
20
+ const raw = fs.readFileSync(filePath, "utf8").trim();
21
+ if (!raw || raw === "No findings.") {
22
+ return { findings: [] };
23
+ }
24
+ try {
25
+ return JSON.parse(raw);
26
+ }
27
+ catch {
28
+ return { findings: [] };
29
+ }
30
+ }
31
+ function groupBySeverity(findings) {
32
+ const groups = {
33
+ critical: [],
34
+ high: [],
35
+ medium: [],
36
+ low: []
37
+ };
38
+ for (const f of findings) {
39
+ groups[f.severity]?.push(f);
40
+ }
41
+ return groups;
42
+ }
43
+ function renderSummaryTable(groups) {
44
+ const lines = [];
45
+ lines.push("| Severity | Count |");
46
+ lines.push("|----------|-------|");
47
+ for (const sev of SEVERITY_ORDER) {
48
+ const count = groups[sev].length;
49
+ if (count > 0) {
50
+ lines.push(`| ${SEVERITY_EMOJI[sev]} ${sev.toUpperCase()} | ${count} |`);
51
+ }
52
+ }
53
+ return lines.join("\n");
54
+ }
55
+ function renderFindingRow(f) {
56
+ const location = f.line ? `\`${f.file}:${f.line}\`` : `\`${f.file}\``;
57
+ const owasp = f.owasp ? ` β€” ${f.owasp}` : "";
58
+ const pkg = f.category === "dependency" && f.packageName
59
+ ? ` πŸ“¦ \`${f.packageName}${f.packageVersion ? `@${f.packageVersion}` : ""}\``
60
+ : "";
61
+ const rec = f.recommendation ? `\n > πŸ’‘ ${f.recommendation}` : "";
62
+ return `- **${f.title}** (${location})${owasp}${pkg}\n ${f.description}${rec}`;
63
+ }
64
+ function renderMarkdown(result) {
65
+ const lines = [];
66
+ const total = result.findings.length;
67
+ lines.push("## πŸ”’ OpenSecurity Scan Results");
68
+ lines.push("");
69
+ if (total === 0) {
70
+ lines.push("βœ… **No security findings detected.** Great job!");
71
+ return lines.join("\n");
72
+ }
73
+ const groups = groupBySeverity(result.findings);
74
+ const criticalCount = groups.critical.length + groups.high.length;
75
+ if (criticalCount > 0) {
76
+ lines.push(`> ⚠️ **${criticalCount} critical/high severity issue${criticalCount > 1 ? "s" : ""} found.** Please review before merging.`);
77
+ }
78
+ else {
79
+ lines.push(`> ℹ️ **${total} finding${total > 1 ? "s" : ""} detected.** No critical issues.`);
80
+ }
81
+ lines.push("");
82
+ lines.push("### Summary");
83
+ lines.push("");
84
+ lines.push(renderSummaryTable(groups));
85
+ lines.push("");
86
+ for (const sev of SEVERITY_ORDER) {
87
+ const items = groups[sev];
88
+ if (items.length === 0)
89
+ continue;
90
+ lines.push(`### ${SEVERITY_EMOJI[sev]} ${sev.toUpperCase()} (${items.length})`);
91
+ lines.push("");
92
+ const shown = items.slice(0, MAX_FINDINGS_PER_SEVERITY);
93
+ for (const f of shown) {
94
+ lines.push(renderFindingRow(f));
95
+ }
96
+ if (items.length > MAX_FINDINGS_PER_SEVERITY) {
97
+ const remaining = items.length - MAX_FINDINGS_PER_SEVERITY;
98
+ lines.push(`\n<details><summary>…and ${remaining} more ${sev} finding${remaining > 1 ? "s" : ""}</summary>\n`);
99
+ for (const f of items.slice(MAX_FINDINGS_PER_SEVERITY)) {
100
+ lines.push(renderFindingRow(f));
101
+ }
102
+ lines.push("\n</details>");
103
+ }
104
+ lines.push("");
105
+ }
106
+ lines.push("---");
107
+ lines.push("*Generated by [OpenSecurity](https://github.com/opensecurity) πŸ›‘οΈ*");
108
+ return lines.join("\n");
109
+ }
110
+ // --- Main ---
111
+ const args = process.argv.slice(2);
112
+ const inputFile = args[0];
113
+ if (!inputFile) {
114
+ console.error("Usage: node pr-comment.js <scan-results.json>");
115
+ process.exit(1);
116
+ }
117
+ const result = loadResults(inputFile);
118
+ console.log(renderMarkdown(result));
@@ -0,0 +1,150 @@
1
+ /**
2
+ * CLI UX utilities: progress spinner, verbose logging, and formatting.
3
+ */
4
+ const SPINNER_FRAMES = ["β ‹", "β ™", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ‡", "⠏"];
5
+ const SPINNER_INTERVAL = 80;
6
+ export class Logger {
7
+ level;
8
+ constructor(options = {}) {
9
+ if (options.silent) {
10
+ this.level = "silent";
11
+ }
12
+ else if (options.verbose) {
13
+ this.level = "verbose";
14
+ }
15
+ else {
16
+ this.level = "normal";
17
+ }
18
+ }
19
+ info(message) {
20
+ if (this.level === "silent")
21
+ return;
22
+ console.error(`β„Ή ${message}`);
23
+ }
24
+ verbose(message) {
25
+ if (this.level !== "verbose")
26
+ return;
27
+ console.error(` ${dim(message)}`);
28
+ }
29
+ success(message) {
30
+ if (this.level === "silent")
31
+ return;
32
+ console.error(`βœ… ${message}`);
33
+ }
34
+ warn(message) {
35
+ if (this.level === "silent")
36
+ return;
37
+ console.error(`⚠️ ${message}`);
38
+ }
39
+ error(message) {
40
+ console.error(`❌ ${message}`);
41
+ }
42
+ }
43
+ export class Spinner {
44
+ frame = 0;
45
+ timer = null;
46
+ message;
47
+ stream;
48
+ active = false;
49
+ constructor(message) {
50
+ this.message = message;
51
+ this.stream = process.stderr;
52
+ }
53
+ start() {
54
+ if (!this.stream.isTTY)
55
+ return;
56
+ this.active = true;
57
+ this.render();
58
+ this.timer = setInterval(() => this.render(), SPINNER_INTERVAL);
59
+ }
60
+ update(message) {
61
+ this.message = message;
62
+ }
63
+ pause() {
64
+ if (this.timer) {
65
+ clearInterval(this.timer);
66
+ this.timer = null;
67
+ }
68
+ if (this.active) {
69
+ this.clearLine();
70
+ }
71
+ }
72
+ resume() {
73
+ if (!this.active || !this.stream.isTTY)
74
+ return;
75
+ if (this.timer)
76
+ return;
77
+ this.render();
78
+ this.timer = setInterval(() => this.render(), SPINNER_INTERVAL);
79
+ }
80
+ stop(finalMessage) {
81
+ if (this.timer) {
82
+ clearInterval(this.timer);
83
+ this.timer = null;
84
+ }
85
+ if (this.active) {
86
+ this.clearLine();
87
+ if (finalMessage) {
88
+ this.stream.write(`${finalMessage}\n`);
89
+ }
90
+ }
91
+ this.active = false;
92
+ }
93
+ render() {
94
+ const symbol = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
95
+ this.frame += 1;
96
+ this.clearLine();
97
+ this.stream.write(`${symbol} ${this.message}`);
98
+ }
99
+ clearLine() {
100
+ this.stream.write("\r\x1b[K");
101
+ }
102
+ }
103
+ /**
104
+ * Format duration in human-readable form.
105
+ */
106
+ export function formatDuration(ms) {
107
+ if (ms < 1000)
108
+ return `${Math.round(ms)}ms`;
109
+ const seconds = ms / 1000;
110
+ if (seconds < 60)
111
+ return `${seconds.toFixed(1)}s`;
112
+ const minutes = Math.floor(seconds / 60);
113
+ const remainingSeconds = Math.round(seconds % 60);
114
+ return `${minutes}m ${remainingSeconds}s`;
115
+ }
116
+ /**
117
+ * Format a count with plural suffix.
118
+ */
119
+ export function pluralize(count, singular, plural) {
120
+ return count === 1 ? `${count} ${singular}` : `${count} ${plural ?? singular + "s"}`;
121
+ }
122
+ /**
123
+ * Dim text using ANSI escape codes (stderr only).
124
+ */
125
+ function dim(text) {
126
+ return `\x1b[2m${text}\x1b[0m`;
127
+ }
128
+ /**
129
+ * Bold text using ANSI escape codes.
130
+ */
131
+ export function bold(text) {
132
+ return `\x1b[1m${text}\x1b[0m`;
133
+ }
134
+ /**
135
+ * Colored severity label.
136
+ */
137
+ export function severityColor(severity) {
138
+ switch (severity) {
139
+ case "critical":
140
+ return `\x1b[31m${severity.toUpperCase()}\x1b[0m`; // red
141
+ case "high":
142
+ return `\x1b[33m${severity.toUpperCase()}\x1b[0m`; // yellow
143
+ case "medium":
144
+ return `\x1b[36m${severity.toUpperCase()}\x1b[0m`; // cyan
145
+ case "low":
146
+ return `\x1b[34m${severity.toUpperCase()}\x1b[0m`; // blue
147
+ default:
148
+ return severity.toUpperCase();
149
+ }
150
+ }
package/dist/proxy.js ADDED
@@ -0,0 +1,93 @@
1
+ import http from "node:http";
2
+ import { URL, pathToFileURL } from "node:url";
3
+ const DEFAULT_PORT = 8787;
4
+ const DEFAULT_OPENAI_BASE = "https://api.openai.com";
5
+ export async function startProxyServer(options = {}) {
6
+ const port = options.port ?? Number(process.env.OPENSECURITY_PROXY_PORT ?? DEFAULT_PORT);
7
+ const openaiBase = process.env.OPENSECURITY_OPENAI_BASE ?? DEFAULT_OPENAI_BASE;
8
+ const proxyApiKey = process.env.OPENSECURITY_PROXY_API_KEY;
9
+ if (!proxyApiKey || !proxyApiKey.trim()) {
10
+ throw new Error("OPENSECURITY_PROXY_API_KEY is required to run the OAuth backend.");
11
+ }
12
+ const server = http.createServer(async (req, res) => {
13
+ try {
14
+ if (req.method !== "POST") {
15
+ res.writeHead(405);
16
+ res.end("Method Not Allowed");
17
+ return;
18
+ }
19
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
20
+ if (url.pathname !== "/v1/responses" && url.pathname !== "/v1/chat/completions") {
21
+ res.writeHead(404);
22
+ res.end("Not Found");
23
+ return;
24
+ }
25
+ const auth = req.headers.authorization ?? "";
26
+ if (!auth.startsWith("Bearer ")) {
27
+ res.writeHead(401);
28
+ res.end("Missing Authorization Bearer token.");
29
+ return;
30
+ }
31
+ const bearerToken = auth.slice("Bearer ".length).trim();
32
+ const apiKey = resolveApiKey(bearerToken, proxyApiKey);
33
+ if (!bearerToken.startsWith("sk-")) {
34
+ await validateOauthToken(bearerToken);
35
+ }
36
+ const body = await readRequestBody(req);
37
+ const upstreamUrl = `${openaiBase}${url.pathname}${url.search}`;
38
+ const upstreamRes = await fetch(upstreamUrl, {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": req.headers["content-type"] ?? "application/json",
42
+ Authorization: `Bearer ${apiKey}`
43
+ },
44
+ body: body.length ? new Uint8Array(body) : undefined
45
+ });
46
+ const responseBody = await upstreamRes.arrayBuffer();
47
+ res.writeHead(upstreamRes.status, {
48
+ "Content-Type": upstreamRes.headers.get("content-type") ?? "application/json"
49
+ });
50
+ res.end(Buffer.from(responseBody));
51
+ }
52
+ catch (err) {
53
+ res.writeHead(500);
54
+ res.end(err?.message ?? "Proxy error.");
55
+ }
56
+ });
57
+ await new Promise((resolve) => server.listen(port, resolve));
58
+ console.log(`OpenSecurity OAuth proxy listening on http://localhost:${port}`);
59
+ console.log("Forwarding OpenAI API requests for Codex OAuth tokens.");
60
+ }
61
+ function resolveApiKey(token, proxyApiKey) {
62
+ if (proxyApiKey && proxyApiKey.trim()) {
63
+ return proxyApiKey.trim();
64
+ }
65
+ if (!token.startsWith("sk-")) {
66
+ throw new Error("Proxy requires OPENSECURITY_PROXY_API_KEY to call OpenAI with OAuth tokens.");
67
+ }
68
+ return token;
69
+ }
70
+ async function validateOauthToken(token) {
71
+ const res = await fetch("https://auth.openai.com/userinfo", {
72
+ headers: { Authorization: `Bearer ${token}` }
73
+ });
74
+ if (!res.ok) {
75
+ const text = await res.text();
76
+ throw new Error(`OAuth token validation failed: ${res.status} ${text}`);
77
+ }
78
+ }
79
+ function readRequestBody(req) {
80
+ return new Promise((resolve, reject) => {
81
+ const chunks = [];
82
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
83
+ req.on("end", () => resolve(Buffer.concat(chunks)));
84
+ req.on("error", reject);
85
+ });
86
+ }
87
+ const isEntryPoint = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
88
+ if (isEntryPoint) {
89
+ startProxyServer().catch((err) => {
90
+ console.error(err?.message ?? err);
91
+ process.exit(1);
92
+ });
93
+ }
@@ -0,0 +1,177 @@
1
+ export const DEFAULT_RULES = [
2
+ {
3
+ id: "js-eval-injection",
4
+ name: "Eval Injection",
5
+ description: "Untrusted data reaches eval or Function",
6
+ severity: "high",
7
+ owasp: "A03:2021 Injection",
8
+ sources: [
9
+ { id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
10
+ { id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
11
+ { id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } },
12
+ { id: "src-readline", name: "readline.question", matcher: { callee: "readline.question" } }
13
+ ],
14
+ sinks: [
15
+ { id: "sink-eval", name: "eval", matcher: { callee: "eval" } },
16
+ { id: "sink-function", name: "Function", matcher: { callee: "Function" } }
17
+ ],
18
+ sanitizers: [
19
+ { id: "san-sanitize", name: "sanitize", matcher: { callee: "sanitize" } },
20
+ { id: "san-escape", name: "escape", matcher: { callee: "escape" } },
21
+ { id: "san-validator-escape", name: "validator.escape", matcher: { callee: "validator.escape" } }
22
+ ]
23
+ },
24
+ {
25
+ id: "js-command-injection",
26
+ name: "Command Injection",
27
+ description: "Untrusted data reaches child_process execution",
28
+ severity: "critical",
29
+ owasp: "A03:2021 Injection",
30
+ sources: [
31
+ { id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
32
+ { id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
33
+ { id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
34
+ ],
35
+ sinks: [
36
+ { id: "sink-exec", name: "child_process.exec", matcher: { callee: "child_process.exec" } },
37
+ { id: "sink-execsync", name: "child_process.execSync", matcher: { callee: "child_process.execSync" } },
38
+ { id: "sink-spawn", name: "child_process.spawn", matcher: { callee: "child_process.spawn" } },
39
+ { id: "sink-spawnsync", name: "child_process.spawnSync", matcher: { callee: "child_process.spawnSync" } },
40
+ { id: "sink-execfile", name: "child_process.execFile", matcher: { callee: "child_process.execFile" } },
41
+ { id: "sink-execfilesync", name: "child_process.execFileSync", matcher: { callee: "child_process.execFileSync" } }
42
+ ],
43
+ sanitizers: [
44
+ { id: "san-shellescape", name: "shellescape", matcher: { callee: "shellescape" } },
45
+ { id: "san-escapeshellarg", name: "escapeShellArg", matcher: { callee: "escapeShellArg" } }
46
+ ]
47
+ },
48
+ {
49
+ id: "js-ssrf",
50
+ name: "Server-Side Request Forgery",
51
+ description: "Untrusted data reaches network request",
52
+ severity: "high",
53
+ owasp: "A10:2021 Server-Side Request Forgery",
54
+ sources: [
55
+ { id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
56
+ { id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
57
+ { id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
58
+ ],
59
+ sinks: [
60
+ { id: "sink-fetch", name: "fetch", matcher: { callee: "fetch" } },
61
+ { id: "sink-axios", name: "axios", matcher: { callee: "axios" } },
62
+ { id: "sink-axios-get", name: "axios.get", matcher: { callee: "axios.get" } },
63
+ { id: "sink-axios-post", name: "axios.post", matcher: { callee: "axios.post" } },
64
+ { id: "sink-axios-request", name: "axios.request", matcher: { callee: "axios.request" } },
65
+ { id: "sink-got", name: "got", matcher: { callee: "got" } },
66
+ { id: "sink-got-get", name: "got.get", matcher: { callee: "got.get" } },
67
+ { id: "sink-got-post", name: "got.post", matcher: { callee: "got.post" } },
68
+ { id: "sink-undici-request", name: "undici.request", matcher: { callee: "undici.request" } },
69
+ { id: "sink-undici-fetch", name: "undici.fetch", matcher: { callee: "undici.fetch" } },
70
+ { id: "sink-http-get", name: "http.get", matcher: { callee: "http.get" } },
71
+ { id: "sink-https-get", name: "https.get", matcher: { callee: "https.get" } },
72
+ { id: "sink-request", name: "request", matcher: { callee: "request" } },
73
+ { id: "sink-request-get", name: "request.get", matcher: { callee: "request.get" } },
74
+ { id: "sink-request-post", name: "request.post", matcher: { callee: "request.post" } },
75
+ { id: "sink-superagent-get", name: "superagent.get", matcher: { callee: "superagent.get" } },
76
+ { id: "sink-superagent-post", name: "superagent.post", matcher: { callee: "superagent.post" } }
77
+ ],
78
+ sanitizers: [
79
+ { id: "san-sanitizeurl", name: "sanitizeUrl", matcher: { callee: "sanitizeUrl" } },
80
+ { id: "san-validateurl", name: "validateUrl", matcher: { callee: "validateUrl" } },
81
+ { id: "san-encodeuri", name: "encodeURI", matcher: { callee: "encodeURI" } },
82
+ { id: "san-encodeuricomp", name: "encodeURIComponent", matcher: { callee: "encodeURIComponent" } }
83
+ ]
84
+ },
85
+ {
86
+ id: "js-path-traversal",
87
+ name: "Path Traversal",
88
+ description: "Untrusted data reaches filesystem APIs",
89
+ severity: "high",
90
+ owasp: "A01:2021 Broken Access Control",
91
+ sources: [
92
+ { id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
93
+ { id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
94
+ { id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } },
95
+ { id: "src-readline", name: "readline.question", matcher: { callee: "readline.question" } }
96
+ ],
97
+ sinks: [
98
+ { id: "sink-readfile", name: "fs.readFile", matcher: { callee: "fs.readFile" } },
99
+ { id: "sink-readfilesync", name: "fs.readFileSync", matcher: { callee: "fs.readFileSync" } },
100
+ { id: "sink-writefile", name: "fs.writeFile", matcher: { callee: "fs.writeFile" } },
101
+ { id: "sink-writefilesync", name: "fs.writeFileSync", matcher: { callee: "fs.writeFileSync" } },
102
+ { id: "sink-appendfile", name: "fs.appendFile", matcher: { callee: "fs.appendFile" } },
103
+ { id: "sink-appendfilesync", name: "fs.appendFileSync", matcher: { callee: "fs.appendFileSync" } },
104
+ { id: "sink-createreadstream", name: "fs.createReadStream", matcher: { callee: "fs.createReadStream" } },
105
+ { id: "sink-createwritestream", name: "fs.createWriteStream", matcher: { callee: "fs.createWriteStream" } },
106
+ { id: "sink-readdir", name: "fs.readdir", matcher: { callee: "fs.readdir" } },
107
+ { id: "sink-readdirsync", name: "fs.readdirSync", matcher: { callee: "fs.readdirSync" } },
108
+ { id: "sink-stat", name: "fs.stat", matcher: { callee: "fs.stat" } },
109
+ { id: "sink-statsync", name: "fs.statSync", matcher: { callee: "fs.statSync" } },
110
+ { id: "sink-lstat", name: "fs.lstat", matcher: { callee: "fs.lstat" } },
111
+ { id: "sink-lstatsync", name: "fs.lstatSync", matcher: { callee: "fs.lstatSync" } },
112
+ { id: "sink-rm", name: "fs.rm", matcher: { callee: "fs.rm" } },
113
+ { id: "sink-rmsync", name: "fs.rmSync", matcher: { callee: "fs.rmSync" } },
114
+ { id: "sink-unlink", name: "fs.unlink", matcher: { callee: "fs.unlink" } },
115
+ { id: "sink-unlinksync", name: "fs.unlinkSync", matcher: { callee: "fs.unlinkSync" } },
116
+ { id: "sink-rmdir", name: "fs.rmdir", matcher: { callee: "fs.rmdir" } },
117
+ { id: "sink-rmdirsync", name: "fs.rmdirSync", matcher: { callee: "fs.rmdirSync" } }
118
+ ],
119
+ sanitizers: [
120
+ { id: "san-path-normalize", name: "path.normalize", matcher: { callee: "path.normalize" } },
121
+ { id: "san-path-resolve", name: "path.resolve", matcher: { callee: "path.resolve" } },
122
+ { id: "san-path-join", name: "path.join", matcher: { callee: "path.join" } }
123
+ ]
124
+ },
125
+ {
126
+ id: "js-sqli",
127
+ name: "SQL Injection",
128
+ description: "Untrusted data reaches database query execution",
129
+ severity: "critical",
130
+ owasp: "A03:2021 Injection",
131
+ sources: [
132
+ { id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
133
+ { id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
134
+ { id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
135
+ ],
136
+ sinks: [
137
+ { id: "sink-mysql-query", name: "mysql.query", matcher: { callee: "mysql.query" } },
138
+ { id: "sink-mysql2-query", name: "mysql2.query", matcher: { callee: "mysql2.query" } },
139
+ { id: "sink-pg-query", name: "pg.query", matcher: { callee: "pg.query" } },
140
+ { id: "sink-client-query", name: "client.query", matcher: { callee: "client.query" } },
141
+ { id: "sink-pool-query", name: "pool.query", matcher: { callee: "pool.query" } },
142
+ { id: "sink-connection-query", name: "connection.query", matcher: { callee: "connection.query" } },
143
+ { id: "sink-db-query", name: "db.query", matcher: { callee: "db.query" } },
144
+ { id: "sink-sequelize-query", name: "sequelize.query", matcher: { callee: "sequelize.query" } },
145
+ { id: "sink-knex-raw", name: "knex.raw", matcher: { callee: "knex.raw" } }
146
+ ],
147
+ sanitizers: [
148
+ { id: "san-sql-escape", name: "escape", matcher: { callee: "escape" } },
149
+ { id: "san-sql-escapeid", name: "escapeId", matcher: { callee: "escapeId" } },
150
+ { id: "san-sql-parameterize", name: "parameterize", matcher: { callee: "parameterize" } }
151
+ ]
152
+ },
153
+ {
154
+ id: "js-xss-template",
155
+ name: "XSS (Server Templates)",
156
+ description: "Untrusted data reaches server response rendering",
157
+ severity: "high",
158
+ owasp: "A03:2021 Injection",
159
+ sources: [
160
+ { id: "src-getUserInput", name: "getUserInput", matcher: { callee: "getUserInput" } },
161
+ { id: "src-req-param", name: "req.param", matcher: { callee: "req.param" } },
162
+ { id: "src-prompt", name: "prompt", matcher: { callee: "prompt" } }
163
+ ],
164
+ sinks: [
165
+ { id: "sink-res-send", name: "res.send", matcher: { callee: "res.send" } },
166
+ { id: "sink-res-write", name: "res.write", matcher: { callee: "res.write" } },
167
+ { id: "sink-res-end", name: "res.end", matcher: { callee: "res.end" } },
168
+ { id: "sink-res-render", name: "res.render", matcher: { callee: "res.render" } },
169
+ { id: "sink-reply-send", name: "reply.send", matcher: { callee: "reply.send" } }
170
+ ],
171
+ sanitizers: [
172
+ { id: "san-escape", name: "escape", matcher: { callee: "escape" } },
173
+ { id: "san-encodeuri", name: "encodeURI", matcher: { callee: "encodeURI" } },
174
+ { id: "san-encodeuricomp", name: "encodeURIComponent", matcher: { callee: "encodeURIComponent" } }
175
+ ]
176
+ }
177
+ ];
@@ -0,0 +1,14 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { DEFAULT_RULES } from "./defaultRules.js";
4
+ export async function loadRules(rulesPath, cwd) {
5
+ if (!rulesPath)
6
+ return DEFAULT_RULES;
7
+ const resolved = path.isAbsolute(rulesPath) ? rulesPath : path.join(cwd, rulesPath);
8
+ const raw = await fs.readFile(resolved, "utf8");
9
+ const parsed = JSON.parse(raw);
10
+ if (!Array.isArray(parsed)) {
11
+ throw new Error("Rules file must be a JSON array");
12
+ }
13
+ return parsed;
14
+ }