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/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  parseModule,
6
6
  resolveImportPath,
7
7
  resolveOptions
8
- } from "./chunk-GQSNZNKN.js";
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.1.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",