From 5910504aebb7b82e12ece4ebbf3e36dbe81352c5 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 27 May 2025 19:52:18 -0700 Subject: [PATCH] `bun pm audit` -> `bun audit` (#19944) --- docs/cli/pm.md | 8 --- docs/install/audit.md | 8 +-- src/cli.zig | 19 +++++- src/cli/audit_command.zig | 33 +++++++--- src/cli/package_manager_command.zig | 4 -- src/install/install.zig | 64 +++++++++++++++---- .../__snapshots__/bun-audit.test.ts.snap | 10 +-- test/cli/install/bun-audit.test.ts | 6 +- test/internal/ban-words.test.ts | 2 +- 9 files changed, 106 insertions(+), 48 deletions(-) diff --git a/docs/cli/pm.md b/docs/cli/pm.md index 51b1807e39..620a08ff9f 100644 --- a/docs/cli/pm.md +++ b/docs/cli/pm.md @@ -95,14 +95,6 @@ To print the hash stored in the current lockfile: $ bun pm hash-print ``` -## audit - -To run a security audit for packages in bun.lock or bun.lockb - -```bash -$ bun pm audit -``` - ## cache To print the path to Bun's global module cache: diff --git a/docs/install/audit.md b/docs/install/audit.md index a3b615b7b5..71845c29dd 100644 --- a/docs/install/audit.md +++ b/docs/install/audit.md @@ -1,9 +1,9 @@ -`bun pm audit` checks your installed packages for known security vulnerabilities. +`bun audit` checks your installed packages for known security vulnerabilities. Run the command in a project with a `bun.lock` file: ```bash -$ bun pm audit +$ bun audit ``` Bun sends the list of installed packages and versions to NPM, and prints a report of any vulnerabilities that were found. Packages installed from registries other than the default registry are skipped. @@ -29,9 +29,9 @@ To update all dependencies to the latest versions (including breaking changes): Use the `--json` flag to print the raw JSON response from the registry instead of the formatted report: ```bash -$ bun pm audit --json +$ bun audit --json ``` ### Exit code -`bun pm audit` will exit with code `0` if no vulnerabilities are found and `1` if the report lists any vulnerabilities. This will still happen even if `--json` is passed. +`bun audit` will exit with code `0` if no vulnerabilities are found and `1` if the report lists any vulnerabilities. This will still happen even if `--json` is passed. diff --git a/src/cli.zig b/src/cli.zig index aec4813566..54b808efd3 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1365,6 +1365,7 @@ pub const HelpCommand = struct { \\ add {s:<16} Add a dependency to package.json (bun a) \\ remove {s:<16} Remove a dependency from package.json (bun rm) \\ update {s:<16} Update outdated dependencies + \\ audit Check installed packages for vulnerabilities \\ outdated Display latest versions of outdated dependencies \\ link [\] Register or link a local npm package \\ unlink Unregister a local npm package @@ -1757,6 +1758,7 @@ pub const Command = struct { RootCommandMatcher.case("outdated") => .OutdatedCommand, RootCommandMatcher.case("publish") => .PublishCommand, + RootCommandMatcher.case("audit") => .AuditCommand, // These are reserved for future use by Bun, so that someone // doing `bun deploy` to run a script doesn't accidentally break @@ -1906,6 +1908,13 @@ pub const Command = struct { try PublishCommand.exec(ctx); return; }, + .AuditCommand => { + if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .AuditCommand) unreachable; + const ctx = try Command.init(allocator, log, .AuditCommand); + + try AuditCommand.exec(ctx); + unreachable; + }, .BunxCommand => { if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .BunxCommand) unreachable; const ctx = try Command.init(allocator, log, .BunxCommand); @@ -2333,6 +2342,7 @@ pub const Command = struct { PatchCommitCommand, OutdatedCommand, PublishCommand, + AuditCommand, /// Used by crash reports. /// @@ -2366,6 +2376,7 @@ pub const Command = struct { .PatchCommitCommand => 'z', .OutdatedCommand => 'o', .PublishCommand => 'k', + .AuditCommand => 'A', }; } @@ -2617,10 +2628,11 @@ pub const Command = struct { , .{}); Output.flush(); }, - .OutdatedCommand, .PublishCommand => { + .OutdatedCommand, .PublishCommand, .AuditCommand => { Install.PackageManager.CommandLineArguments.printHelp(switch (cmd) { .OutdatedCommand => .outdated, .PublishCommand => .publish, + .AuditCommand => .audit, }); }, else => { @@ -2641,6 +2653,7 @@ pub const Command = struct { .PatchCommitCommand, .OutdatedCommand, .PublishCommand, + .AuditCommand, => true, else => false, }; @@ -2660,6 +2673,7 @@ pub const Command = struct { .PatchCommitCommand, .OutdatedCommand, .PublishCommand, + .AuditCommand, => true, else => false, }; @@ -2681,6 +2695,7 @@ pub const Command = struct { .RunAsNodeCommand = true, .OutdatedCommand = true, .PublishCommand = true, + .AuditCommand = true, }); pub const always_loads_config: std.EnumArray(Tag, bool) = std.EnumArray(Tag, bool).initDefault(false, .{ @@ -2696,6 +2711,7 @@ pub const Command = struct { .BunxCommand = true, .OutdatedCommand = true, .PublishCommand = true, + .AuditCommand = true, }); pub const uses_global_options: std.EnumArray(Tag, bool) = std.EnumArray(Tag, bool).initDefault(true, .{ @@ -2712,6 +2728,7 @@ pub const Command = struct { .BunxCommand = false, .OutdatedCommand = false, .PublishCommand = false, + .AuditCommand = false, }); }; }; diff --git a/src/cli/audit_command.zig b/src/cli/audit_command.zig index 0ee26b2502..2b47fb208c 100644 --- a/src/cli/audit_command.zig +++ b/src/cli/audit_command.zig @@ -62,19 +62,32 @@ const AuditResult = struct { }; pub const AuditCommand = struct { + pub fn exec(ctx: Command.Context) !noreturn { + const cli = try PackageManager.CommandLineArguments.parse(ctx.allocator, .audit); + const manager, _ = PackageManager.init(ctx, cli, .audit) catch |err| { + if (err == error.MissingPackageJSON) { + var cwd_buf: bun.PathBuffer = undefined; + if (bun.getcwd(&cwd_buf)) |cwd| { + Output.errGeneric("No package.json was found for directory \"{s}\"", .{cwd}); + } else |_| { + Output.errGeneric("No package.json was found", .{}); + } + Output.note("Run \"bun init\" to initialize a project", .{}); + Global.exit(1); + } + + return err; + }; + + const code = try audit(ctx, manager, manager.options.json_output); + Global.exit(code); + } + /// Returns the exit code of the command. 0 if no vulnerabilities were found, 1 if vulnerabilities were found. /// The exception is when you pass --json, it will simply return 0 as that was considered a successful "request /// for the audit information" - pub fn exec(ctx: Command.Context, pm: *PackageManager, args: [][:0]u8) bun.OOM!u32 { - var json_output = false; - for (args) |arg| { - if (std.mem.eql(u8, arg, "--json")) { - json_output = true; - break; - } - } - - Output.prettyError(comptime Output.prettyFmt("bun pm audit v" ++ Global.package_json_version_with_sha ++ "\n", true), .{}); + pub fn audit(ctx: Command.Context, pm: *PackageManager, json_output: bool) bun.OOM!u32 { + Output.prettyError(comptime Output.prettyFmt("bun audit v" ++ Global.package_json_version_with_sha ++ "\n", true), .{}); Output.flush(); const load_lockfile = pm.lockfile.loadFromCwd(pm, ctx.allocator, ctx.log, true); diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index 8ff8dec10c..82432e07ad 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -20,7 +20,6 @@ const TrustCommand = @import("./pm_trusted_command.zig").TrustCommand; const DefaultTrustedCommand = @import("./pm_trusted_command.zig").DefaultTrustedCommand; const Environment = bun.Environment; pub const PackCommand = @import("./pack_command.zig").PackCommand; -pub const AuditCommand = @import("./audit_command.zig").AuditCommand; const Npm = Install.Npm; const PmViewCommand = @import("./pm_view_command.zig"); const File = bun.sys.File; @@ -247,9 +246,6 @@ pub const PackageManagerCommand = struct { _ = try pm.lockfile.hasMetaHashChanged(true, pm.lockfile.packages.len); Global.exit(0); - } else if (strings.eqlComptime(subcommand, "audit")) { - const code = try AuditCommand.exec(ctx, pm, args); - Global.exit(code); } else if (strings.eqlComptime(subcommand, "cache")) { var dir: bun.PathBuffer = undefined; var fd = pm.getCacheDirectory(); diff --git a/src/install/install.zig b/src/install/install.zig index f0d2257b44..f68d4aa7a8 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -26,7 +26,6 @@ const FD = bun.FD; const JSON = bun.JSON; const JSPrinter = bun.js_printer; - const Api = @import("../api/schema.zig").Api; const Path = bun.path; const Command = @import("../cli.zig").Command; @@ -59,7 +58,6 @@ pub const TextLockfile = @import("./lockfile/bun.lock.zig"); pub const PatchedDep = Lockfile.PatchedDep; const Walker = @import("../walker_skippable.zig"); - pub const bun_hash_tag = ".bun-tag-"; pub const max_hex_hash_len: comptime_int = brk: { var buf: [128]u8 = undefined; @@ -8668,6 +8666,7 @@ pub const PackageManager = struct { outdated, pack, publish, + audit, // bin, // hash, @@ -8692,6 +8691,16 @@ pub const PackageManager = struct { .outdated => true, .install => true, // .pack => true, + // .add => true, + else => false, + }; + } + + pub fn supportsJsonOutput(this: Subcommand) bool { + return switch (this) { + .audit, + .pm, + => true, else => false, }; } @@ -9740,11 +9749,16 @@ pub const PackageManager = struct { }); const outdated_params: []const ParamType = &(shared_params ++ [_]ParamType{ - clap.parseParam("--json Output outdated information in JSON format") catch unreachable, + // clap.parseParam("--json Output outdated information in JSON format") catch unreachable, clap.parseParam("-F, --filter ... Display outdated dependencies for each matching workspace") catch unreachable, clap.parseParam(" ... Package patterns to filter by") catch unreachable, }); + const audit_params: []const ParamType = &([_]ParamType{ + clap.parseParam(" ... Check installed packages for vulnerabilities") catch unreachable, + clap.parseParam("--json Output in JSON format") catch unreachable, + }); + const pack_params: []const ParamType = &(shared_params ++ [_]ParamType{ // clap.parseParam("--filter ... Pack each matching workspace") catch unreachable, clap.parseParam("--destination The directory the tarball will be saved in") catch unreachable, @@ -10105,6 +10119,28 @@ pub const PackageManager = struct { Output.pretty("\n\n" ++ outro_text ++ "\n", .{}); Output.flush(); }, + .audit => { + const intro_text = + \\Usage: bun audit [flags] + ; + + const outro_text = + \\Examples: + \\ Check installed packages for vulnerabilities. + \\ bun audit + \\ + \\ Output package vulnerabilities in JSON format. + \\ bun audit --json + \\ + ; + + Output.pretty("\n" ++ intro_text ++ "\n", .{}); + Output.pretty("\nFlags:", .{}); + Output.flush(); + clap.simpleHelp(PackageManager.audit_params); + Output.pretty("\n\n" ++ outro_text ++ "\n", .{}); + Output.flush(); + }, } } @@ -10124,6 +10160,10 @@ pub const PackageManager = struct { .outdated => outdated_params, .pack => pack_params, .publish => publish_params, + + // TODO: we will probably want to do this for other *_params. this way extra params + // are not included in the help text + .audit => shared_params ++ audit_params, }; var diag = clap.Diagnostic{}; @@ -10132,10 +10172,9 @@ pub const PackageManager = struct { .diagnostic = &diag, .allocator = allocator, }) catch |err| { - clap.help(Output.errorWriter(), params) catch {}; - Output.errorWriter().writeAll("\n") catch {}; + printHelp(subcommand); diag.report(Output.errorWriter(), err) catch {}; - return err; + Global.exit(1); }; if (args.flag("--help")) { @@ -10204,16 +10243,17 @@ pub const PackageManager = struct { cli.filters = args.options("--filter"); } - if (comptime subcommand == .outdated) { - // fake --dry-run, we don't actually resolve+clean the lockfile - cli.dry_run = true; + if (comptime subcommand.supportsJsonOutput()) { cli.json_output = args.flag("--json"); } + if (comptime subcommand == .outdated) { + // fake --dry-run, we don't actually resolve+clean the lockfile + cli.dry_run = true; + // cli.json_output = args.flag("--json"); + } + if (comptime subcommand == .pack or subcommand == .pm or subcommand == .publish) { - if (comptime subcommand == .pm) { - cli.json_output = args.flag("--json"); - } if (comptime subcommand != .publish) { if (args.option("--destination")) |dest| { cli.pack_destination = dest; diff --git a/test/cli/install/__snapshots__/bun-audit.test.ts.snap b/test/cli/install/__snapshots__/bun-audit.test.ts.snap index 154c97cfe1..d7b514d5f5 100644 --- a/test/cli/install/__snapshots__/bun-audit.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-audit.test.ts.snap @@ -1,6 +1,6 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP -exports[`\`bun pm audit\` should exit code 1 when there are vulnerabilities: bun-audit-expect-vulnerabilities-found 1`] = ` +exports[`\`bun audit\` should exit code 1 when there are vulnerabilities: bun-audit-expect-vulnerabilities-found 1`] = ` "minimist <0.2.4 express › mkdirp › minimist critical: Prototype Pollution in minimist - https://github.com/advisories/GHSA-xvch-5gv4-984h @@ -78,9 +78,9 @@ To update all dependencies to the latest versions (including breaking changes): " `; -exports[`\`bun pm audit\` should print valid JSON and exit 0 when --json is passed and there are no vulnerabilities: bun-audit-expect-valid-json-stdout-report-no-vulnerabilities 1`] = `{}`; +exports[`\`bun audit\` should print valid JSON and exit 0 when --json is passed and there are no vulnerabilities: bun-audit-expect-valid-json-stdout-report-no-vulnerabilities 1`] = `{}`; -exports[`\`bun pm audit\` should print valid JSON and exit 1 when --json is passed and there are vulnerabilities: bun-audit-expect-valid-json-stdout-report-vulnerabilities 1`] = ` +exports[`\`bun audit\` should print valid JSON and exit 1 when --json is passed and there are vulnerabilities: bun-audit-expect-valid-json-stdout-report-vulnerabilities 1`] = ` { "base64-url": [ { @@ -410,7 +410,7 @@ exports[`\`bun pm audit\` should print valid JSON and exit 1 when --json is pass } `; -exports[`\`bun pm audit\` should exit 1 and behave exactly the same when there are vulnerabilities when only devDependencies are specified: bun-audit-expect-vulnerabilities-found 1`] = ` +exports[`\`bun audit\` should exit 1 and behave exactly the same when there are vulnerabilities when only devDependencies are specified: bun-audit-expect-vulnerabilities-found 1`] = ` "ms <2.0.0 (direct dependency) moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability - https://github.com/advisories/GHSA-w9mr-4mfr-499f @@ -427,7 +427,7 @@ To update all dependencies to the latest versions (including breaking changes): " `; -exports[`\`bun pm audit\` when a project has some safe dependencies and some vulnerable dependencies, we should not print the safe dependencies: bun-audit-expect-vulnerabilities-found 1`] = ` +exports[`\`bun audit\` when a project has some safe dependencies and some vulnerable dependencies, we should not print the safe dependencies: bun-audit-expect-vulnerabilities-found 1`] = ` "ms <2.0.0 (direct dependency) moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability - https://github.com/advisories/GHSA-w9mr-4mfr-499f diff --git a/test/cli/install/bun-audit.test.ts b/test/cli/install/bun-audit.test.ts index 037a3370f1..f1c841ec58 100644 --- a/test/cli/install/bun-audit.test.ts +++ b/test/cli/install/bun-audit.test.ts @@ -47,9 +47,9 @@ function doAuditTest( }, ) { test(label, async () => { - const dir = tempDirWithFiles("bun-test-pm-audit-" + label.replace(/[^a-zA-Z0-9]/g, "-"), options.files); + const dir = tempDirWithFiles("bun-test-audit-" + label.replace(/[^a-zA-Z0-9]/g, "-"), options.files); - const cmd = [bunExe(), "pm", "audit", ...(options.args ?? [])]; + const cmd = [bunExe(), "audit", ...(options.args ?? [])]; const url = server.url.toString().slice(0, -1); @@ -87,7 +87,7 @@ function doAuditTest( }); } -describe("`bun pm audit`", () => { +describe("`bun audit`", () => { doAuditTest("should fail with no package.json", { exitCode: 1, files: { diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index f7b281c989..4a27bdf7fc 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -34,7 +34,7 @@ const words: Record [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 241, regex: true }, "usingnamespace": { reason: "Zig 0.15 will remove `usingnamespace`" }, - "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1851 }, + "catch unreachable": { reason: "For out-of-memory, prefer 'catch bun.outOfMemory()'", limit: 1852 }, "std.fs.Dir": { reason: "Prefer bun.sys + bun.FD instead of std.fs", limit: 180 }, "std.fs.cwd": { reason: "Prefer bun.FD.cwd()", limit: 103 },