server-action-audit 0.1.0 → 0.2.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/README.fr.md +2 -1
- package/README.md +19 -0
- package/dist/{chunk-GQSNZNKN.js → chunk-RCEEE6TI.js} +2 -2
- package/dist/{chunk-GQSNZNKN.js.map → chunk-RCEEE6TI.js.map} +1 -1
- package/dist/cli.js +160 -18
- package/dist/cli.js.map +1 -1
- package/dist/eslint.cjs +1 -1
- package/dist/eslint.cjs.map +1 -1
- package/dist/eslint.js +1 -1
- package/dist/index.cjs +115 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +108 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
parseModule,
|
|
6
6
|
resolveImportPath,
|
|
7
7
|
resolveOptions
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RCEEE6TI.js";
|
|
9
9
|
|
|
10
10
|
// src/scan.ts
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
@@ -178,6 +178,51 @@ function formatGithub(report) {
|
|
|
178
178
|
lines.push(`server-action-audit: ${summary.actions} Server Actions, ${summary.unprotected} unprotected, ${summary.protected} protected, ${summary.public} public, ${summary.unvalidated} with unvalidated input.`);
|
|
179
179
|
return lines.join("\n");
|
|
180
180
|
}
|
|
181
|
+
function formatMarkdown(report, options = {}) {
|
|
182
|
+
const { summary } = report;
|
|
183
|
+
const fileCount = report.files.length;
|
|
184
|
+
const total = `${summary.actions} Server Action${summary.actions === 1 ? "" : "s"} in ${fileCount} file${fileCount === 1 ? "" : "s"}`;
|
|
185
|
+
const lines = ["### server-action-audit", ""];
|
|
186
|
+
const found = report.files.flatMap(
|
|
187
|
+
({ file, actions }) => actions.filter((action) => action.status === "unprotected" || action.validated === false).map((action) => ({ file, action }))
|
|
188
|
+
);
|
|
189
|
+
if (!summary.actions) lines.push("No Server Actions found.");
|
|
190
|
+
else if (!found.length) lines.push(`\u2705 ${total}, no problems found.`);
|
|
191
|
+
else {
|
|
192
|
+
lines.push(
|
|
193
|
+
[
|
|
194
|
+
summary.unprotected ? `**\u274C ${summary.unprotected} unprotected**` : "0 unprotected",
|
|
195
|
+
`${summary.protected} protected`,
|
|
196
|
+
`${summary.public} public`,
|
|
197
|
+
summary.unvalidated ? `**\u26A0\uFE0F ${summary.unvalidated} with unvalidated input**` : "0 with unvalidated input"
|
|
198
|
+
].join(" \xB7 ") + ` (${total})`,
|
|
199
|
+
"",
|
|
200
|
+
"| | Action | Where | Problem |",
|
|
201
|
+
"| --- | --- | --- | --- |"
|
|
202
|
+
);
|
|
203
|
+
const cell = (text) => text.replace(/\|/g, "\\|");
|
|
204
|
+
const segment = (part) => encodeURIComponent(part).replace(/[()!'*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
205
|
+
const limit = options.limit ?? 50;
|
|
206
|
+
for (const { file, action } of found.slice(0, limit)) {
|
|
207
|
+
const where = `${file}:${action.line}`;
|
|
208
|
+
const link = options.sourceUrl ? `[${cell(where)}](${options.sourceUrl}${file.split("/").map(segment).join("/")}#L${action.line})` : `\`${cell(where)}\``;
|
|
209
|
+
const unprotected = action.status === "unprotected";
|
|
210
|
+
const problem = [unprotected && "No authorization check", action.validated === false && (unprotected ? "input not validated" : "Input not validated")].filter(Boolean).join(", ");
|
|
211
|
+
lines.push(`| ${unprotected ? "\u274C" : "\u26A0\uFE0F"} | \`${cell(action.name)}\` | ${link} | ${problem} |`);
|
|
212
|
+
}
|
|
213
|
+
if (found.length > limit) lines.push("", `\u2026and ${found.length - limit} more. Run \`npx server-action-audit\` to see them all.`);
|
|
214
|
+
if (summary.unprotected) {
|
|
215
|
+
lines.push(
|
|
216
|
+
"",
|
|
217
|
+
"Every Server Action is a public endpoint: anyone can call it with a POST request. Check the session inside each action, or mark intentional ones with `// @public-action`. See the [Next.js data security guide](https://nextjs.org/docs/app/guides/data-security)."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (report.errors.length) {
|
|
222
|
+
lines.push("", `Skipped ${report.errors.length} file${report.errors.length === 1 ? "" : "s"} that could not be parsed: ${report.errors.map((error) => `\`${error.file}\``).join(", ")}.`);
|
|
223
|
+
}
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|
|
181
226
|
function formatSarif(report, version) {
|
|
182
227
|
const rules = [
|
|
183
228
|
{
|
|
@@ -217,18 +262,79 @@ function formatSarif(report, version) {
|
|
|
217
262
|
2
|
|
218
263
|
);
|
|
219
264
|
}
|
|
265
|
+
|
|
266
|
+
// src/github.ts
|
|
267
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
268
|
+
var COMMENT_MARKER = "<!-- server-action-audit -->";
|
|
269
|
+
function pullRequestFromEnv(env = process.env) {
|
|
270
|
+
if (!env.GITHUB_REPOSITORY || !env.GITHUB_EVENT_PATH) return "not running in GitHub Actions";
|
|
271
|
+
if (!env.GITHUB_TOKEN) return "GITHUB_TOKEN is not set";
|
|
272
|
+
const event = JSON.parse(readFileSync2(env.GITHUB_EVENT_PATH, "utf8"));
|
|
273
|
+
if (!event.pull_request) return `the "${env.GITHUB_EVENT_NAME ?? "unknown"}" event is not about a pull request`;
|
|
274
|
+
return {
|
|
275
|
+
apiUrl: env.GITHUB_API_URL || "https://api.github.com",
|
|
276
|
+
serverUrl: env.GITHUB_SERVER_URL || "https://github.com",
|
|
277
|
+
repository: env.GITHUB_REPOSITORY,
|
|
278
|
+
number: event.pull_request.number,
|
|
279
|
+
headSha: event.pull_request.head?.sha,
|
|
280
|
+
token: env.GITHUB_TOKEN
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function upsertComment(pr, body, options) {
|
|
284
|
+
const request = async (method, path, payload) => {
|
|
285
|
+
const response = await (options.fetch ?? fetch)(`${pr.apiUrl}${path}`, {
|
|
286
|
+
method,
|
|
287
|
+
headers: {
|
|
288
|
+
accept: "application/vnd.github+json",
|
|
289
|
+
authorization: `Bearer ${pr.token}`,
|
|
290
|
+
"user-agent": `server-action-audit/${VERSION}`,
|
|
291
|
+
"x-github-api-version": "2022-11-28",
|
|
292
|
+
...payload ? { "content-type": "application/json" } : {}
|
|
293
|
+
},
|
|
294
|
+
body: payload ? JSON.stringify(payload) : void 0
|
|
295
|
+
});
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
const message = await response.json().then(
|
|
298
|
+
(error) => error.message,
|
|
299
|
+
() => void 0
|
|
300
|
+
);
|
|
301
|
+
throw Object.assign(new Error(`${method} ${path} answered ${response.status}${message ? ` ${message}` : ""}`), { status: response.status });
|
|
302
|
+
}
|
|
303
|
+
return await response.json();
|
|
304
|
+
};
|
|
305
|
+
let previous;
|
|
306
|
+
for (let page = 1; !previous; page++) {
|
|
307
|
+
const comments = await request("GET", `/repos/${pr.repository}/issues/${pr.number}/comments?per_page=100&page=${page}`);
|
|
308
|
+
previous = comments.find((comment) => comment.body?.startsWith(COMMENT_MARKER));
|
|
309
|
+
if (comments.length < 100) break;
|
|
310
|
+
}
|
|
311
|
+
const text = `${COMMENT_MARKER}
|
|
312
|
+
${body}`;
|
|
313
|
+
if (previous) {
|
|
314
|
+
if (previous.body === text) return "skipped";
|
|
315
|
+
await request("PATCH", `/repos/${pr.repository}/issues/comments/${previous.id}`, { body: text });
|
|
316
|
+
return "updated";
|
|
317
|
+
}
|
|
318
|
+
if (options.clean) return "skipped";
|
|
319
|
+
await request("POST", `/repos/${pr.repository}/issues/${pr.number}/comments`, { body: text });
|
|
320
|
+
return "created";
|
|
321
|
+
}
|
|
220
322
|
export {
|
|
323
|
+
COMMENT_MARKER,
|
|
221
324
|
VERSION,
|
|
222
325
|
analyzeModule,
|
|
223
326
|
createNodeResolver,
|
|
224
327
|
formatGithub,
|
|
225
328
|
formatJson,
|
|
329
|
+
formatMarkdown,
|
|
226
330
|
formatPretty,
|
|
227
331
|
formatSarif,
|
|
228
332
|
loadConfig,
|
|
229
333
|
parseModule,
|
|
334
|
+
pullRequestFromEnv,
|
|
230
335
|
resolveImportPath,
|
|
231
336
|
resolveOptions,
|
|
232
|
-
scanProject
|
|
337
|
+
scanProject,
|
|
338
|
+
upsertComment
|
|
233
339
|
};
|
|
234
340
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/scan.ts","../src/format.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { join, relative, sep } from 'node:path'\nimport { analyzeModule, type ServerAction } from './analyze.js'\nimport { resolveOptions, type AuditOptions } from './options.js'\nimport { createNodeResolver, parseModule } from './resolve.js'\n\nexport interface FileReport {\n /** Relative to the project root, with forward slashes. */\n file: string\n actions: ServerAction[]\n}\n\nexport interface ProjectReport {\n root: string\n files: FileReport[]\n /** Files that mention \"use server\" but could not be parsed. */\n errors: { file: string; message: string }[]\n summary: { actions: number; protected: number; unprotected: number; public: number; unvalidated: number }\n}\n\nconst SKIP_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', 'out', 'coverage', '.turbo', '.vercel', '.output', '.svelte-kit', '.cache'])\nconst SOURCE = /\\.(?:[cm]?[jt]s|[jt]sx)$/\n\nfunction globToRegExp(glob: string): RegExp {\n let pattern = ''\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i]!\n if (char === '*' && glob[i + 1] === '*') {\n pattern += glob[i + 2] === '/' ? '(?:.*/)?' : '.*'\n i += glob[i + 2] === '/' ? 2 : 1\n } else if (char === '*') pattern += '[^/]*'\n else if (char === '?') pattern += '[^/]'\n else pattern += char.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${pattern}$`)\n}\n\nfunction* sourceFiles(dir: string): Generator<string> {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) yield* sourceFiles(join(dir, entry.name))\n } else if (entry.isFile() && SOURCE.test(entry.name) && !entry.name.endsWith('.d.ts')) {\n yield join(dir, entry.name)\n }\n }\n}\n\n/** Options from `server-action-audit.config.json`, or the `serverActionAudit` key of package.json. */\nexport function loadConfig(root: string, configPath?: string): AuditOptions {\n if (configPath) return JSON.parse(readFileSync(configPath, 'utf8')) as AuditOptions\n const file = join(root, 'server-action-audit.config.json')\n if (existsSync(file)) return JSON.parse(readFileSync(file, 'utf8')) as AuditOptions\n const pkg = join(root, 'package.json')\n if (existsSync(pkg)) {\n const config = (JSON.parse(readFileSync(pkg, 'utf8')) as { serverActionAudit?: AuditOptions }).serverActionAudit\n if (config) return config\n }\n return {}\n}\n\nexport function scanProject(root: string, options: AuditOptions = {}): ProjectReport {\n const resolved = resolveOptions(options)\n const ignore = resolved.ignore.map(globToRegExp)\n const resolver = createNodeResolver()\n const report: ProjectReport = { root, files: [], errors: [], summary: { actions: 0, protected: 0, unprotected: 0, public: 0, unvalidated: 0 } }\n\n for (const path of sourceFiles(root)) {\n const file = relative(root, path).split(sep).join('/')\n if (ignore.some((pattern) => pattern.test(file))) continue\n const text = readFileSync(path, 'utf8')\n if (!text.includes('use server')) continue\n let actions: ServerAction[]\n try {\n actions = analyzeModule({ ast: parseModule(text, path), text, filename: path }, resolved, resolver)\n } catch (error) {\n report.errors.push({ file, message: (error as Error).message })\n continue\n }\n if (!actions.length) continue\n report.files.push({ file, actions })\n for (const action of actions) {\n report.summary.actions++\n report.summary[action.status]++\n if (action.validated === false) report.summary.unvalidated++\n }\n }\n report.files.sort((a, b) => a.file.localeCompare(b.file))\n return report\n}\n","import type { ServerAction } from './analyze.js'\nimport type { ProjectReport } from './scan.js'\n\nconst useColor = process.env.FORCE_COLOR ? true : !process.env.NO_COLOR && !!process.stdout.isTTY\nconst paint = (code: string) => (text: string) => (useColor ? `\\x1b[${code}m${text}\\x1b[0m` : text)\nconst red = paint('31')\nconst green = paint('32')\nconst yellow = paint('33')\nconst cyan = paint('36')\nconst dim = paint('2')\nconst bold = paint('1')\n\nfunction describe(action: ServerAction): string {\n const parts: string[] = []\n if (action.status === 'protected') parts.push(dim(action.reason ?? ''))\n if (action.status === 'public') parts.push(cyan(`public (${action.reason})`))\n if (action.status === 'unprotected') parts.push(red('no auth check'))\n if (action.validated === false) parts.push(yellow('input not validated'))\n return parts.join(dim(' · '))\n}\n\nfunction symbol(action: ServerAction): string {\n if (action.status === 'unprotected') return red('✗')\n if (action.validated === false) return yellow('!')\n if (action.status === 'public') return cyan('○')\n return green('✓')\n}\n\nexport function formatPretty(report: ProjectReport, options: { quiet?: boolean } = {}): string {\n const { summary } = report\n const lines: string[] = []\n const fileCount = report.files.length\n lines.push(bold(`server-action-audit · ${summary.actions} Server Action${summary.actions === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`), '')\n\n for (const { file, actions } of report.files) {\n const shown = options.quiet ? actions.filter((a) => a.status === 'unprotected' || a.validated === false) : actions\n if (!shown.length) continue\n lines.push(bold(file))\n const width = Math.min(32, Math.max(...shown.map((a) => a.name.length)))\n for (const action of shown) {\n const where = dim(`${action.line}:${action.column}`.padEnd(7))\n lines.push(` ${symbol(action)} ${action.name.padEnd(width)} ${where} ${describe(action)}`)\n if (action.status === 'unprotected' && action.hint) lines.push(` ${dim(action.hint)}`)\n }\n lines.push('')\n }\n\n for (const error of report.errors) lines.push(yellow(`skipped ${error.file}: ${error.message}`))\n if (report.errors.length) lines.push('')\n\n if (!summary.actions) {\n lines.push('No Server Actions found.')\n return lines.join('\\n')\n }\n lines.push(\n [\n summary.unprotected ? red(`✗ ${summary.unprotected} unprotected`) : green('✗ 0 unprotected'),\n green(`✓ ${summary.protected} protected`),\n cyan(`○ ${summary.public} public`),\n summary.unvalidated ? yellow(`! ${summary.unvalidated} with unvalidated input`) : dim('! 0 with unvalidated input'),\n ].join(dim(' · ')),\n )\n if (summary.unprotected) {\n lines.push(\n '',\n dim('Every Server Action is a public endpoint: anyone can call it with a POST request.'),\n dim('Check the session inside each action, or mark intentional ones with // @public-action.'),\n dim('https://nextjs.org/docs/app/guides/data-security'),\n )\n }\n return lines.join('\\n')\n}\n\nexport function formatJson(report: ProjectReport): string {\n return JSON.stringify(\n {\n summary: report.summary,\n files: report.files.map(({ file, actions }) => ({\n file,\n actions: actions.map(({ node: _node, ...action }) => action),\n })),\n errors: report.errors,\n },\n null,\n 2,\n )\n}\n\nconst AUTH_MESSAGE = (name: string) =>\n `Server Action \"${name}\" has no authorization check: anyone can call it with a POST request. Check the session inside the action, or mark it with // @public-action if it is public on purpose.`\nconst VALIDATION_MESSAGE = (name: string) =>\n `Server Action \"${name}\" uses its arguments without validating them. They come straight from the client: parse them with a schema (Zod, Valibot…).`\n\ninterface Finding {\n rule: 'require-auth' | 'require-input-validation'\n level: 'error' | 'warning'\n file: string\n action: ServerAction\n message: string\n}\n\nfunction findings(report: ProjectReport): Finding[] {\n const found: Finding[] = []\n for (const { file, actions } of report.files) {\n for (const action of actions) {\n if (action.status === 'unprotected') {\n found.push({ rule: 'require-auth', level: 'error', file, action, message: AUTH_MESSAGE(action.name) + (action.hint ? ` ${action.hint}` : '') })\n }\n if (action.validated === false) {\n found.push({ rule: 'require-input-validation', level: 'warning', file, action, message: VALIDATION_MESSAGE(action.name) })\n }\n }\n }\n return found\n}\n\n/** GitHub Actions workflow commands: the findings show up as annotations on the pull request. */\nexport function formatGithub(report: ProjectReport): string {\n const data = (text: string) => text.replace(/%/g, '%25').replace(/\\r/g, '%0D').replace(/\\n/g, '%0A')\n const property = (text: string) => data(text).replace(/:/g, '%3A').replace(/,/g, '%2C')\n const lines = findings(report).map(\n ({ rule, level, file, action, message }) =>\n `::${level} file=${property(file)},line=${action.line},col=${action.column},title=${property(`server-action-audit/${rule}`)}::${data(message)}`,\n )\n const { summary } = report\n lines.push(`server-action-audit: ${summary.actions} Server Actions, ${summary.unprotected} unprotected, ${summary.protected} protected, ${summary.public} public, ${summary.unvalidated} with unvalidated input.`)\n return lines.join('\\n')\n}\n\n/** SARIF 2.1.0, for GitHub code scanning and other security dashboards. */\nexport function formatSarif(report: ProjectReport, version: string): string {\n const rules = [\n {\n id: 'require-auth',\n name: 'RequireAuth',\n shortDescription: { text: 'Server Action without an authorization check' },\n fullDescription: { text: 'Every Next.js Server Action is a public endpoint. This one does not check who is calling it.' },\n helpUri: 'https://github.com/i6moons/server-action-audit#readme',\n defaultConfiguration: { level: 'error' },\n properties: { tags: ['security', 'nextjs'], 'security-severity': '7.5', precision: 'medium' },\n },\n {\n id: 'require-input-validation',\n name: 'RequireInputValidation',\n shortDescription: { text: 'Server Action without input validation' },\n fullDescription: { text: 'The action uses arguments sent by the client without parsing them with a schema.' },\n helpUri: 'https://github.com/i6moons/server-action-audit#readme',\n defaultConfiguration: { level: 'warning' },\n properties: { tags: ['security', 'nextjs'], 'security-severity': '4.0', precision: 'medium' },\n },\n ]\n const results = findings(report).map(({ rule, level, file, action, message }) => ({\n ruleId: rule,\n ruleIndex: rule === 'require-auth' ? 0 : 1,\n level,\n message: { text: message },\n locations: [{ physicalLocation: { artifactLocation: { uri: file }, region: { startLine: action.line, startColumn: action.column } } }],\n partialFingerprints: { actionName: `${file}#${action.name}` },\n }))\n return JSON.stringify(\n {\n $schema: 'https://json.schemastore.org/sarif-2.1.0.json',\n version: '2.1.0',\n runs: [{ tool: { driver: { name: 'server-action-audit', version, informationUri: 'https://github.com/i6moons/server-action-audit', rules } }, results }],\n },\n null,\n 2,\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/scan.ts","../src/format.ts","../src/github.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { join, relative, sep } from 'node:path'\nimport { analyzeModule, type ServerAction } from './analyze.js'\nimport { resolveOptions, type AuditOptions } from './options.js'\nimport { createNodeResolver, parseModule } from './resolve.js'\n\nexport interface FileReport {\n /** Relative to the project root, with forward slashes. */\n file: string\n actions: ServerAction[]\n}\n\nexport interface ProjectReport {\n root: string\n files: FileReport[]\n /** Files that mention \"use server\" but could not be parsed. */\n errors: { file: string; message: string }[]\n summary: { actions: number; protected: number; unprotected: number; public: number; unvalidated: number }\n}\n\nconst SKIP_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', 'out', 'coverage', '.turbo', '.vercel', '.output', '.svelte-kit', '.cache'])\nconst SOURCE = /\\.(?:[cm]?[jt]s|[jt]sx)$/\n\nfunction globToRegExp(glob: string): RegExp {\n let pattern = ''\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i]!\n if (char === '*' && glob[i + 1] === '*') {\n pattern += glob[i + 2] === '/' ? '(?:.*/)?' : '.*'\n i += glob[i + 2] === '/' ? 2 : 1\n } else if (char === '*') pattern += '[^/]*'\n else if (char === '?') pattern += '[^/]'\n else pattern += char.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${pattern}$`)\n}\n\nfunction* sourceFiles(dir: string): Generator<string> {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) yield* sourceFiles(join(dir, entry.name))\n } else if (entry.isFile() && SOURCE.test(entry.name) && !entry.name.endsWith('.d.ts')) {\n yield join(dir, entry.name)\n }\n }\n}\n\n/** Options from `server-action-audit.config.json`, or the `serverActionAudit` key of package.json. */\nexport function loadConfig(root: string, configPath?: string): AuditOptions {\n if (configPath) return JSON.parse(readFileSync(configPath, 'utf8')) as AuditOptions\n const file = join(root, 'server-action-audit.config.json')\n if (existsSync(file)) return JSON.parse(readFileSync(file, 'utf8')) as AuditOptions\n const pkg = join(root, 'package.json')\n if (existsSync(pkg)) {\n const config = (JSON.parse(readFileSync(pkg, 'utf8')) as { serverActionAudit?: AuditOptions }).serverActionAudit\n if (config) return config\n }\n return {}\n}\n\nexport function scanProject(root: string, options: AuditOptions = {}): ProjectReport {\n const resolved = resolveOptions(options)\n const ignore = resolved.ignore.map(globToRegExp)\n const resolver = createNodeResolver()\n const report: ProjectReport = { root, files: [], errors: [], summary: { actions: 0, protected: 0, unprotected: 0, public: 0, unvalidated: 0 } }\n\n for (const path of sourceFiles(root)) {\n const file = relative(root, path).split(sep).join('/')\n if (ignore.some((pattern) => pattern.test(file))) continue\n const text = readFileSync(path, 'utf8')\n if (!text.includes('use server')) continue\n let actions: ServerAction[]\n try {\n actions = analyzeModule({ ast: parseModule(text, path), text, filename: path }, resolved, resolver)\n } catch (error) {\n report.errors.push({ file, message: (error as Error).message })\n continue\n }\n if (!actions.length) continue\n report.files.push({ file, actions })\n for (const action of actions) {\n report.summary.actions++\n report.summary[action.status]++\n if (action.validated === false) report.summary.unvalidated++\n }\n }\n report.files.sort((a, b) => a.file.localeCompare(b.file))\n return report\n}\n","import type { ServerAction } from './analyze.js'\nimport type { ProjectReport } from './scan.js'\n\nconst useColor = process.env.FORCE_COLOR ? true : !process.env.NO_COLOR && !!process.stdout.isTTY\nconst paint = (code: string) => (text: string) => (useColor ? `\\x1b[${code}m${text}\\x1b[0m` : text)\nconst red = paint('31')\nconst green = paint('32')\nconst yellow = paint('33')\nconst cyan = paint('36')\nconst dim = paint('2')\nconst bold = paint('1')\n\nfunction describe(action: ServerAction): string {\n const parts: string[] = []\n if (action.status === 'protected') parts.push(dim(action.reason ?? ''))\n if (action.status === 'public') parts.push(cyan(`public (${action.reason})`))\n if (action.status === 'unprotected') parts.push(red('no auth check'))\n if (action.validated === false) parts.push(yellow('input not validated'))\n return parts.join(dim(' · '))\n}\n\nfunction symbol(action: ServerAction): string {\n if (action.status === 'unprotected') return red('✗')\n if (action.validated === false) return yellow('!')\n if (action.status === 'public') return cyan('○')\n return green('✓')\n}\n\nexport function formatPretty(report: ProjectReport, options: { quiet?: boolean } = {}): string {\n const { summary } = report\n const lines: string[] = []\n const fileCount = report.files.length\n lines.push(bold(`server-action-audit · ${summary.actions} Server Action${summary.actions === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`), '')\n\n for (const { file, actions } of report.files) {\n const shown = options.quiet ? actions.filter((a) => a.status === 'unprotected' || a.validated === false) : actions\n if (!shown.length) continue\n lines.push(bold(file))\n const width = Math.min(32, Math.max(...shown.map((a) => a.name.length)))\n for (const action of shown) {\n const where = dim(`${action.line}:${action.column}`.padEnd(7))\n lines.push(` ${symbol(action)} ${action.name.padEnd(width)} ${where} ${describe(action)}`)\n if (action.status === 'unprotected' && action.hint) lines.push(` ${dim(action.hint)}`)\n }\n lines.push('')\n }\n\n for (const error of report.errors) lines.push(yellow(`skipped ${error.file}: ${error.message}`))\n if (report.errors.length) lines.push('')\n\n if (!summary.actions) {\n lines.push('No Server Actions found.')\n return lines.join('\\n')\n }\n lines.push(\n [\n summary.unprotected ? red(`✗ ${summary.unprotected} unprotected`) : green('✗ 0 unprotected'),\n green(`✓ ${summary.protected} protected`),\n cyan(`○ ${summary.public} public`),\n summary.unvalidated ? yellow(`! ${summary.unvalidated} with unvalidated input`) : dim('! 0 with unvalidated input'),\n ].join(dim(' · ')),\n )\n if (summary.unprotected) {\n lines.push(\n '',\n dim('Every Server Action is a public endpoint: anyone can call it with a POST request.'),\n dim('Check the session inside each action, or mark intentional ones with // @public-action.'),\n dim('https://nextjs.org/docs/app/guides/data-security'),\n )\n }\n return lines.join('\\n')\n}\n\nexport function formatJson(report: ProjectReport): string {\n return JSON.stringify(\n {\n summary: report.summary,\n files: report.files.map(({ file, actions }) => ({\n file,\n actions: actions.map(({ node: _node, ...action }) => action),\n })),\n errors: report.errors,\n },\n null,\n 2,\n )\n}\n\nconst AUTH_MESSAGE = (name: string) =>\n `Server Action \"${name}\" has no authorization check: anyone can call it with a POST request. Check the session inside the action, or mark it with // @public-action if it is public on purpose.`\nconst VALIDATION_MESSAGE = (name: string) =>\n `Server Action \"${name}\" uses its arguments without validating them. They come straight from the client: parse them with a schema (Zod, Valibot…).`\n\ninterface Finding {\n rule: 'require-auth' | 'require-input-validation'\n level: 'error' | 'warning'\n file: string\n action: ServerAction\n message: string\n}\n\nfunction findings(report: ProjectReport): Finding[] {\n const found: Finding[] = []\n for (const { file, actions } of report.files) {\n for (const action of actions) {\n if (action.status === 'unprotected') {\n found.push({ rule: 'require-auth', level: 'error', file, action, message: AUTH_MESSAGE(action.name) + (action.hint ? ` ${action.hint}` : '') })\n }\n if (action.validated === false) {\n found.push({ rule: 'require-input-validation', level: 'warning', file, action, message: VALIDATION_MESSAGE(action.name) })\n }\n }\n }\n return found\n}\n\n/** GitHub Actions workflow commands: the findings show up as annotations on the pull request. */\nexport function formatGithub(report: ProjectReport): string {\n const data = (text: string) => text.replace(/%/g, '%25').replace(/\\r/g, '%0D').replace(/\\n/g, '%0A')\n const property = (text: string) => data(text).replace(/:/g, '%3A').replace(/,/g, '%2C')\n const lines = findings(report).map(\n ({ rule, level, file, action, message }) =>\n `::${level} file=${property(file)},line=${action.line},col=${action.column},title=${property(`server-action-audit/${rule}`)}::${data(message)}`,\n )\n const { summary } = report\n lines.push(`server-action-audit: ${summary.actions} Server Actions, ${summary.unprotected} unprotected, ${summary.protected} protected, ${summary.public} public, ${summary.unvalidated} with unvalidated input.`)\n return lines.join('\\n')\n}\n\n/**\n * Markdown, for a pull request comment or a job summary. With `sourceUrl` (a URL ending in `/` that the\n * report's file paths can be appended to), each location links to the code.\n */\nexport function formatMarkdown(report: ProjectReport, options: { sourceUrl?: string; limit?: number } = {}): string {\n const { summary } = report\n const fileCount = report.files.length\n const total = `${summary.actions} Server Action${summary.actions === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`\n const lines = ['### server-action-audit', '']\n const found = report.files.flatMap(({ file, actions }) =>\n actions.filter((action) => action.status === 'unprotected' || action.validated === false).map((action) => ({ file, action })),\n )\n\n if (!summary.actions) lines.push('No Server Actions found.')\n else if (!found.length) lines.push(`✅ ${total}, no problems found.`)\n else {\n lines.push(\n [\n summary.unprotected ? `**❌ ${summary.unprotected} unprotected**` : '0 unprotected',\n `${summary.protected} protected`,\n `${summary.public} public`,\n summary.unvalidated ? `**⚠️ ${summary.unvalidated} with unvalidated input**` : '0 with unvalidated input',\n ].join(' · ') + ` (${total})`,\n '',\n '| | Action | Where | Problem |',\n '| --- | --- | --- | --- |',\n )\n // Table cells end at a pipe, even inside code spans\n const cell = (text: string) => text.replace(/\\|/g, '\\\\|')\n const segment = (part: string) => encodeURIComponent(part).replace(/[()!'*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)\n const limit = options.limit ?? 50\n for (const { file, action } of found.slice(0, limit)) {\n const where = `${file}:${action.line}`\n const link = options.sourceUrl ? `[${cell(where)}](${options.sourceUrl}${file.split('/').map(segment).join('/')}#L${action.line})` : `\\`${cell(where)}\\``\n const unprotected = action.status === 'unprotected'\n const problem = [unprotected && 'No authorization check', action.validated === false && (unprotected ? 'input not validated' : 'Input not validated')].filter(Boolean).join(', ')\n lines.push(`| ${unprotected ? '❌' : '⚠️'} | \\`${cell(action.name)}\\` | ${link} | ${problem} |`)\n }\n if (found.length > limit) lines.push('', `…and ${found.length - limit} more. Run \\`npx server-action-audit\\` to see them all.`)\n if (summary.unprotected) {\n lines.push(\n '',\n 'Every Server Action is a public endpoint: anyone can call it with a POST request. Check the session inside each action, or mark intentional ones with `// @public-action`. See the [Next.js data security guide](https://nextjs.org/docs/app/guides/data-security).',\n )\n }\n }\n\n if (report.errors.length) {\n lines.push('', `Skipped ${report.errors.length} file${report.errors.length === 1 ? '' : 's'} that could not be parsed: ${report.errors.map((error) => `\\`${error.file}\\``).join(', ')}.`)\n }\n return lines.join('\\n')\n}\n\n/** SARIF 2.1.0, for GitHub code scanning and other security dashboards. */\nexport function formatSarif(report: ProjectReport, version: string): string {\n const rules = [\n {\n id: 'require-auth',\n name: 'RequireAuth',\n shortDescription: { text: 'Server Action without an authorization check' },\n fullDescription: { text: 'Every Next.js Server Action is a public endpoint. This one does not check who is calling it.' },\n helpUri: 'https://github.com/i6moons/server-action-audit#readme',\n defaultConfiguration: { level: 'error' },\n properties: { tags: ['security', 'nextjs'], 'security-severity': '7.5', precision: 'medium' },\n },\n {\n id: 'require-input-validation',\n name: 'RequireInputValidation',\n shortDescription: { text: 'Server Action without input validation' },\n fullDescription: { text: 'The action uses arguments sent by the client without parsing them with a schema.' },\n helpUri: 'https://github.com/i6moons/server-action-audit#readme',\n defaultConfiguration: { level: 'warning' },\n properties: { tags: ['security', 'nextjs'], 'security-severity': '4.0', precision: 'medium' },\n },\n ]\n const results = findings(report).map(({ rule, level, file, action, message }) => ({\n ruleId: rule,\n ruleIndex: rule === 'require-auth' ? 0 : 1,\n level,\n message: { text: message },\n locations: [{ physicalLocation: { artifactLocation: { uri: file }, region: { startLine: action.line, startColumn: action.column } } }],\n partialFingerprints: { actionName: `${file}#${action.name}` },\n }))\n return JSON.stringify(\n {\n $schema: 'https://json.schemastore.org/sarif-2.1.0.json',\n version: '2.1.0',\n runs: [{ tool: { driver: { name: 'server-action-audit', version, informationUri: 'https://github.com/i6moons/server-action-audit', rules } }, results }],\n },\n null,\n 2,\n )\n}\n","import { readFileSync } from 'node:fs'\nimport { VERSION } from './version.js'\n\n/** Hidden at the top of the comment, so the next run edits it instead of adding another one. */\nexport const COMMENT_MARKER = '<!-- server-action-audit -->'\n\nexport interface PullRequest {\n apiUrl: string\n serverUrl: string\n /** owner/name */\n repository: string\n number: number\n headSha?: string\n token: string\n}\n\ninterface IssueComment {\n id: number\n body?: string\n}\n\n/** The pull request a GitHub Actions run is for, from its environment, or why there is none. */\nexport function pullRequestFromEnv(env: NodeJS.ProcessEnv = process.env): PullRequest | string {\n if (!env.GITHUB_REPOSITORY || !env.GITHUB_EVENT_PATH) return 'not running in GitHub Actions'\n if (!env.GITHUB_TOKEN) return 'GITHUB_TOKEN is not set'\n const event = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, 'utf8')) as { pull_request?: { number: number; head?: { sha?: string } } }\n if (!event.pull_request) return `the \"${env.GITHUB_EVENT_NAME ?? 'unknown'}\" event is not about a pull request`\n return {\n apiUrl: env.GITHUB_API_URL || 'https://api.github.com',\n serverUrl: env.GITHUB_SERVER_URL || 'https://github.com',\n repository: env.GITHUB_REPOSITORY,\n number: event.pull_request.number,\n headSha: event.pull_request.head?.sha,\n token: env.GITHUB_TOKEN,\n }\n}\n\n/**\n * Posts the report as a comment on the pull request, or updates the one an earlier run posted.\n * A clean report is only written over an earlier comment: a pull request with nothing to fix gets no comment.\n */\nexport async function upsertComment(\n pr: PullRequest,\n body: string,\n options: { clean: boolean; fetch?: typeof fetch },\n): Promise<'created' | 'updated' | 'skipped'> {\n const request = async <T>(method: string, path: string, payload?: object): Promise<T> => {\n const response = await (options.fetch ?? fetch)(`${pr.apiUrl}${path}`, {\n method,\n headers: {\n accept: 'application/vnd.github+json',\n authorization: `Bearer ${pr.token}`,\n 'user-agent': `server-action-audit/${VERSION}`,\n 'x-github-api-version': '2022-11-28',\n ...(payload ? { 'content-type': 'application/json' } : {}),\n },\n body: payload ? JSON.stringify(payload) : undefined,\n })\n if (!response.ok) {\n const message = await response.json().then(\n (error) => (error as { message?: string }).message,\n () => undefined,\n )\n throw Object.assign(new Error(`${method} ${path} answered ${response.status}${message ? ` ${message}` : ''}`), { status: response.status })\n }\n return (await response.json()) as T\n }\n\n let previous: IssueComment | undefined\n for (let page = 1; !previous; page++) {\n const comments = await request<IssueComment[]>('GET', `/repos/${pr.repository}/issues/${pr.number}/comments?per_page=100&page=${page}`)\n previous = comments.find((comment) => comment.body?.startsWith(COMMENT_MARKER))\n if (comments.length < 100) break\n }\n\n const text = `${COMMENT_MARKER}\\n${body}`\n if (previous) {\n if (previous.body === text) return 'skipped'\n await request('PATCH', `/repos/${pr.repository}/issues/comments/${previous.id}`, { body: text })\n return 'updated'\n }\n if (options.clean) return 'skipped'\n await request('POST', `/repos/${pr.repository}/issues/${pr.number}/comments`, { body: text })\n return 'created'\n}\n"],"mappings":"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","names":["readFileSync","readFileSync"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "server-action-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Find Next.js Server Actions that anyone can call: a CLI and an ESLint plugin that flag \"use server\" functions without an authorization check or input validation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|