const VulnerabilityInfo = struct { severity: []const u8, title: []const u8, url: []const u8, vulnerable_versions: []const u8, id: []const u8, package_name: []const u8, }; const PackageInfo = struct { package_id: u32, name: []const u8, version: []const u8, vulnerabilities: std.array_list.Managed(VulnerabilityInfo), dependents: std.array_list.Managed(DependencyPath), const DependencyPath = struct { path: std.array_list.Managed([]const u8), is_direct: bool, }; }; const AuditResult = struct { vulnerable_packages: bun.StringHashMap(PackageInfo), all_vulnerabilities: std.array_list.Managed(VulnerabilityInfo), allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator) AuditResult { return AuditResult{ .vulnerable_packages = bun.StringHashMap(PackageInfo).init(allocator), .all_vulnerabilities = std.array_list.Managed(VulnerabilityInfo).init(allocator), .allocator = allocator, }; } pub fn deinit(self: *AuditResult) void { var iter = self.vulnerable_packages.iterator(); while (iter.next()) |entry| { entry.value_ptr.vulnerabilities.deinit(); for (entry.value_ptr.dependents.items) |*dependent| { dependent.path.deinit(); } entry.value_ptr.dependents.deinit(); } self.vulnerable_packages.deinit(); self.all_vulnerabilities.deinit(); } }; 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, cli.audit_level, cli.production, cli.audit_ignore_list); 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 audit(ctx: Command.Context, pm: *PackageManager, json_output: bool, audit_level: ?AuditLevel, audit_prod_only: bool, ignore_list: []const []const u8) 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); @import("./package_manager_command.zig").PackageManagerCommand.handleLoadLockfileErrors(load_lockfile, pm); var dependency_tree = try buildDependencyTree(ctx.allocator, pm); defer dependency_tree.deinit(); const packages_result = try collectPackagesForAudit(ctx.allocator, pm, audit_prod_only); defer ctx.allocator.free(packages_result.audit_body); defer { for (packages_result.skipped_packages.items) |package_name| { ctx.allocator.free(package_name); } packages_result.skipped_packages.deinit(); } const response_text = try sendAuditRequest(ctx.allocator, pm, packages_result.audit_body); defer ctx.allocator.free(response_text); if (json_output) { Output.writer().writeAll(response_text) catch {}; Output.writer().writeByte('\n') catch {}; if (response_text.len > 0) { const source = &logger.Source.initPathString("audit-response.json", response_text); var log = logger.Log.init(ctx.allocator); defer log.deinit(); const expr = bun.json.parse(source, &log, ctx.allocator, true) catch { Output.prettyErrorln("error: audit request failed to parse json. Is the registry down?", .{}); return 1; // If we can't parse then safe to assume a similar failure }; // If the response is an empty object, no vulnerabilities if (expr.data == .e_object and expr.data.e_object.properties.len == 0) { return 0; } // If there's any content in the response, there are vulnerabilities return 1; } return 0; } else if (response_text.len > 0) { const exit_code = try printEnhancedAuditReport(ctx.allocator, response_text, pm, &dependency_tree, audit_level, ignore_list); printSkippedPackages(packages_result.skipped_packages); return exit_code; } else { Output.prettyln("No vulnerabilities found", .{}); printSkippedPackages(packages_result.skipped_packages); return 0; } } }; fn printSkippedPackages(skipped_packages: std.array_list.Managed([]const u8)) void { if (skipped_packages.items.len > 0) { Output.pretty("Skipped ", .{}); for (skipped_packages.items, 0..) |package_name, i| { if (i > 0) Output.pretty(", ", .{}); Output.pretty("{s}", .{package_name}); } if (skipped_packages.items.len > 1) { Output.prettyln(" because they do not come from the default registry", .{}); } else { Output.prettyln(" because it does not come from the default registry", .{}); } Output.prettyln("", .{}); } } fn buildDependencyTree(allocator: std.mem.Allocator, pm: *PackageManager) bun.OOM!bun.StringHashMap(std.array_list.Managed([]const u8)) { var dependency_tree = bun.StringHashMap(std.array_list.Managed([]const u8)).init(allocator); const packages = pm.lockfile.packages.slice(); const pkg_names = packages.items(.name); const pkg_dependencies = packages.items(.dependencies); const pkg_resolutions = packages.items(.resolutions); const buf = pm.lockfile.buffers.string_bytes.items; const dependencies = pm.lockfile.buffers.dependencies.items; const resolutions = pm.lockfile.buffers.resolutions.items; for (pkg_names, pkg_dependencies, pkg_resolutions, 0..) |pkg_name, deps, res_list, pkg_idx| { const package_name = pkg_name.slice(buf); if (packages.items(.resolution)[pkg_idx].tag != .npm) continue; const dep_slice = deps.get(dependencies); const res_slice = res_list.get(resolutions); for (dep_slice, res_slice) |_, resolved_pkg_id| { if (resolved_pkg_id >= pkg_names.len) continue; const resolved_name = pkg_names[resolved_pkg_id].slice(buf); const result = try dependency_tree.getOrPut(resolved_name); if (!result.found_existing) { result.key_ptr.* = try allocator.dupe(u8, resolved_name); result.value_ptr.* = std.array_list.Managed([]const u8).init(allocator); } try result.value_ptr.append(try allocator.dupe(u8, package_name)); } } return dependency_tree; } fn buildProductionPackageSet(allocator: std.mem.Allocator, pm: *PackageManager, prod_set: *bun.StringHashMap(void)) bun.OOM!void { const packages = pm.lockfile.packages.slice(); const pkg_names = packages.items(.name); const pkg_dependencies = packages.items(.dependencies); const pkg_resolutions = packages.items(.resolutions); const buf = pm.lockfile.buffers.string_bytes.items; const dependencies = pm.lockfile.buffers.dependencies.items; const resolutions = pm.lockfile.buffers.resolutions.items; const root_id = pm.root_package_id.get(pm.lockfile, pm.workspace_name_hash); var queue = bun.LinearFifo(u32, .Dynamic).init(allocator); defer queue.deinit(); const root_deps = pkg_dependencies[root_id]; const root_resolutions = pkg_resolutions[root_id]; const dep_slice = root_deps.get(dependencies); const res_slice = root_resolutions.get(resolutions); for (dep_slice, res_slice) |dep, resolved_pkg_id| { if (!dep.behavior.isDev() and resolved_pkg_id < packages.len) { const pkg_name = pkg_names[resolved_pkg_id].slice(buf); try prod_set.put(pkg_name, {}); try queue.writeItem(resolved_pkg_id); } } while (queue.readItem()) |current_pkg_id| { const current_deps = pkg_dependencies[current_pkg_id]; const current_resolutions = pkg_resolutions[current_pkg_id]; const current_dep_slice = current_deps.get(dependencies); const current_res_slice = current_resolutions.get(resolutions); for (current_dep_slice, current_res_slice) |dep, resolved_pkg_id| { // Skip devDependencies - they should not be included in production audit if (dep.behavior.isDev()) continue; if (resolved_pkg_id >= pkg_names.len) continue; const pkg_name = pkg_names[resolved_pkg_id].slice(buf); if (!prod_set.contains(pkg_name)) { try prod_set.put(pkg_name, {}); try queue.writeItem(resolved_pkg_id); } } } } fn collectPackagesForAudit(allocator: std.mem.Allocator, pm: *PackageManager, prod_only: bool) bun.OOM!struct { audit_body: []u8, skipped_packages: std.array_list.Managed([]const u8) } { const packages = pm.lockfile.packages.slice(); const pkg_names = packages.items(.name); const pkg_resolutions = packages.items(.resolution); const buf = pm.lockfile.buffers.string_bytes.items; const root_id = pm.root_package_id.get(pm.lockfile, pm.workspace_name_hash); var packages_list = std.array_list.Managed(struct { name: []const u8, versions: std.array_list.Managed([]const u8), }).init(allocator); defer { for (packages_list.items) |item| { allocator.free(item.name); for (item.versions.items) |version| { allocator.free(version); } item.versions.deinit(); } packages_list.deinit(); } var skipped_packages = std.array_list.Managed([]const u8).init(allocator); var prod_packages: ?bun.StringHashMap(void) = null; defer if (prod_packages) |*map| map.deinit(); if (prod_only) { prod_packages = bun.StringHashMap(void).init(allocator); try buildProductionPackageSet(allocator, pm, &prod_packages.?); } for (pkg_names, pkg_resolutions, 0..) |name, res, idx| { if (idx == root_id) continue; if (res.tag != .npm) continue; const name_slice = name.slice(buf); if (prod_only and prod_packages != null) { if (!prod_packages.?.contains(name_slice)) { continue; } } const package_scope = pm.scopeForPackageName(name_slice); if (package_scope.url_hash != pm.options.scope.url_hash) { try skipped_packages.append(try allocator.dupe(u8, name_slice)); continue; } const ver_str = try std.fmt.allocPrint(allocator, "{f}", .{res.value.npm.version.fmt(buf)}); var found_package: ?*@TypeOf(packages_list.items[0]) = null; for (packages_list.items) |*item| { if (std.mem.eql(u8, item.name, name_slice)) { found_package = item; break; } } if (found_package == null) { try packages_list.append(.{ .name = try allocator.dupe(u8, name_slice), .versions = std.array_list.Managed([]const u8).init(allocator), }); found_package = &packages_list.items[packages_list.items.len - 1]; } var version_exists = false; for (found_package.?.versions.items) |existing_ver| { if (std.mem.eql(u8, existing_ver, ver_str)) { version_exists = true; break; } } if (!version_exists) { try found_package.?.versions.append(ver_str); } else { allocator.free(ver_str); } } var body = try MutableString.init(allocator, 1024); body.appendChar('{') catch {}; for (packages_list.items, 0..) |package, pkg_idx| { if (pkg_idx > 0) body.appendChar(',') catch {}; body.appendChar('"') catch {}; body.appendSlice(package.name) catch {}; body.appendChar('"') catch {}; body.appendChar(':') catch {}; body.appendChar('[') catch {}; for (package.versions.items, 0..) |version, ver_idx| { if (ver_idx > 0) body.appendChar(',') catch {}; body.appendChar('"') catch {}; body.appendSlice(version) catch {}; body.appendChar('"') catch {}; } body.appendChar(']') catch {}; } body.appendChar('}') catch {}; return .{ .audit_body = try allocator.dupe(u8, body.slice()), .skipped_packages = skipped_packages, }; } fn sendAuditRequest(allocator: std.mem.Allocator, pm: *PackageManager, body: []const u8) bun.OOM![]u8 { libdeflate.load(); var compressor = libdeflate.Compressor.alloc(6) orelse return error.OutOfMemory; defer compressor.deinit(); const max_compressed_size = compressor.maxBytesNeeded(body, .gzip); const compressed_body = try allocator.alloc(u8, max_compressed_size); defer allocator.free(compressed_body); const compression_result = compressor.gzip(body, compressed_body); const final_compressed_body = compressed_body[0..compression_result.written]; var headers: HeaderBuilder = .{}; headers.count("accept", "application/json"); headers.count("content-type", "application/json"); headers.count("content-encoding", "gzip"); if (pm.options.scope.token.len > 0) { headers.count("authorization", ""); headers.content.cap += "Bearer ".len + pm.options.scope.token.len; } else if (pm.options.scope.auth.len > 0) { headers.count("authorization", ""); headers.content.cap += "Basic ".len + pm.options.scope.auth.len; } try headers.allocate(allocator); headers.append("accept", "application/json"); headers.append("content-type", "application/json"); headers.append("content-encoding", "gzip"); if (pm.options.scope.token.len > 0) { headers.appendFmt("authorization", "Bearer {s}", .{pm.options.scope.token}); } else if (pm.options.scope.auth.len > 0) { headers.appendFmt("authorization", "Basic {s}", .{pm.options.scope.auth}); } const url_str = try std.fmt.allocPrint(allocator, "{s}/-/npm/v1/security/advisories/bulk", .{strings.withoutTrailingSlash(pm.options.scope.url.href)}); defer allocator.free(url_str); const url = URL.parse(url_str); const http_proxy = pm.env.getHttpProxyFor(url); var response_buf = try MutableString.init(allocator, 1024); var req = http.AsyncHTTP.initSync( allocator, .POST, url, headers.entries, headers.content.ptr.?[0..headers.content.len], &response_buf, final_compressed_body, http_proxy, null, .follow, ); const res = req.sendSync() catch |err| { Output.err(err, "audit request failed", .{}); Global.crash(); }; if (res.status_code >= 400) { Output.prettyErrorln("error: audit request failed (status {d})", .{res.status_code}); Global.crash(); } return try allocator.dupe(u8, response_buf.slice()); } fn parseVulnerability(allocator: std.mem.Allocator, package_name: []const u8, vuln: bun.ast.Expr) bun.OOM!VulnerabilityInfo { var vulnerability = VulnerabilityInfo{ .severity = "moderate", .title = "Vulnerability found", .url = "", .vulnerable_versions = "", .id = "", .package_name = try allocator.dupe(u8, package_name), }; if (vuln.data == .e_object) { const props = vuln.data.e_object.properties.slice(); for (props) |prop| { if (prop.key) |key| { if (key.data == .e_string) { const field_name = key.data.e_string.data; if (prop.value) |value| { if (value.data == .e_string) { const field_value = value.data.e_string.data; if (std.mem.eql(u8, field_name, "severity")) { vulnerability.severity = field_value; } else if (std.mem.eql(u8, field_name, "title")) { vulnerability.title = field_value; } else if (std.mem.eql(u8, field_name, "url")) { vulnerability.url = field_value; } else if (std.mem.eql(u8, field_name, "vulnerable_versions")) { vulnerability.vulnerable_versions = field_value; } else if (std.mem.eql(u8, field_name, "id")) { vulnerability.id = field_value; } } else if (value.data == .e_number) { if (std.mem.eql(u8, field_name, "id")) { vulnerability.id = try std.fmt.allocPrint(allocator, "{d}", .{@as(u64, @intFromFloat(value.data.e_number.value))}); } } } } } } } return vulnerability; } fn findDependencyPaths( allocator: std.mem.Allocator, target_package: []const u8, dependency_tree: *const bun.StringHashMap(std.array_list.Managed([]const u8)), pm: *PackageManager, ) bun.OOM!std.array_list.Managed(PackageInfo.DependencyPath) { var paths = std.array_list.Managed(PackageInfo.DependencyPath).init(allocator); const packages = pm.lockfile.packages.slice(); const root_id = pm.root_package_id.get(pm.lockfile, pm.workspace_name_hash); const root_deps = packages.items(.dependencies)[root_id]; const dependencies = pm.lockfile.buffers.dependencies.items; const buf = pm.lockfile.buffers.string_bytes.items; const pkg_names = packages.items(.name); const pkg_resolutions = packages.items(.resolution); const pkg_deps = packages.items(.dependencies); const dep_slice = root_deps.get(dependencies); for (dep_slice) |dependency| { const dep_name = dependency.name.slice(buf); if (std.mem.eql(u8, dep_name, target_package)) { var direct_path = PackageInfo.DependencyPath{ .path = std.array_list.Managed([]const u8).init(allocator), .is_direct = true, }; try direct_path.path.append(try allocator.dupe(u8, target_package)); try paths.append(direct_path); break; } } for (pkg_resolutions, pkg_deps, pkg_names) |resolution, workspace_deps, pkg_name| { if (resolution.tag != .workspace) continue; const workspace_name = pkg_name.slice(buf); const workspace_dep_slice = workspace_deps.get(dependencies); for (workspace_dep_slice) |dependency| { const dep_name = dependency.name.slice(buf); if (std.mem.eql(u8, dep_name, target_package)) { var workspace_path = PackageInfo.DependencyPath{ .path = std.array_list.Managed([]const u8).init(allocator), .is_direct = false, }; const workspace_prefix = try std.fmt.allocPrint(allocator, "workspace:{s}", .{workspace_name}); try workspace_path.path.append(workspace_prefix); try workspace_path.path.append(try allocator.dupe(u8, target_package)); try paths.append(workspace_path); break; } } } var queue: bun.LinearFifo([]const u8, .Dynamic) = bun.LinearFifo([]const u8, .Dynamic).init(allocator); defer queue.deinit(); var visited = bun.StringHashMap(void).init(allocator); defer visited.deinit(); var parent_map = bun.StringHashMap([]const u8).init(allocator); defer parent_map.deinit(); if (dependency_tree.get(target_package)) |dependents| { for (dependents.items) |dependent| { try queue.writeItem(dependent); try parent_map.put(dependent, target_package); } } while (queue.readItem()) |*current| { if (visited.contains(current.*)) continue; try visited.put(current.*, {}); var is_root_dep = false; for (dep_slice) |*dependency| { const dep_name = dependency.name.slice(buf); if (bun.strings.eql(dep_name, current.*)) { is_root_dep = true; break; } } var workspace_name_for_dep: ?[]const u8 = null; for (pkg_resolutions, pkg_deps, pkg_names) |resolution, workspace_deps, pkg_name| { if (resolution.tag != .workspace) continue; const workspace_dep_slice = workspace_deps.get(dependencies); for (workspace_dep_slice) |*dependency| { const dep_name = dependency.name.slice(buf); if (bun.strings.eql(dep_name, current.*)) { workspace_name_for_dep = pkg_name.slice(buf); break; } } if (workspace_name_for_dep != null) break; } if (is_root_dep or workspace_name_for_dep != null) { var path = PackageInfo.DependencyPath{ .path = std.array_list.Managed([]const u8).init(allocator), .is_direct = false, }; var trace = current.*; var seen_in_trace = bun.StringHashMap(void).init(allocator); defer seen_in_trace.deinit(); while (true) { // Check for cycle before processing if (seen_in_trace.contains(trace)) { // Cycle detected, stop tracing break; } // Add to path and mark as seen try path.path.insert(0, try allocator.dupe(u8, trace)); try seen_in_trace.put(trace, {}); // Get parent for next iteration if (parent_map.get(trace)) |parent| { trace = parent; } else { break; } } if (workspace_name_for_dep) |workspace_name| { const workspace_prefix = try std.fmt.allocPrint(allocator, "workspace:{s}", .{workspace_name}); try path.path.insert(0, workspace_prefix); } try paths.append(path); } else { if (dependency_tree.get(current.*)) |dependents| { for (dependents.items) |dependent| { if (!visited.contains(dependent)) { try queue.writeItem(dependent); try parent_map.put(dependent, current.*); } } } } } return paths; } fn printEnhancedAuditReport( allocator: std.mem.Allocator, response_text: []const u8, pm: *PackageManager, dependency_tree: *const bun.StringHashMap(std.array_list.Managed([]const u8)), audit_level: ?AuditLevel, ignore_list: []const []const u8, ) bun.OOM!u32 { const source = &logger.Source.initPathString("audit-response.json", response_text); var log = logger.Log.init(allocator); defer log.deinit(); const expr = bun.json.parse(source, &log, allocator, true) catch { Output.writer().writeAll(response_text) catch {}; Output.writer().writeByte('\n') catch {}; return 1; }; if (expr.data == .e_object and expr.data.e_object.properties.len == 0) { Output.prettyln("No vulnerabilities found", .{}); return 0; } var audit_result = AuditResult.init(allocator); defer audit_result.deinit(); var vuln_counts = struct { low: u32 = 0, moderate: u32 = 0, high: u32 = 0, critical: u32 = 0, }{}; if (expr.data == .e_object) { const properties = expr.data.e_object.properties.slice(); for (properties) |prop| { if (prop.key) |key| { if (key.data == .e_string) { const package_name = key.data.e_string.data; if (prop.value) |value| { if (value.data == .e_array) { const vulns = value.data.e_array.items.slice(); for (vulns) |vuln| { if (vuln.data == .e_object) { const vulnerability = try parseVulnerability(allocator, package_name, vuln); if (audit_level) |level| { if (!level.shouldIncludeSeverity(vulnerability.severity)) { continue; } } if (ignore_list.len > 0) { var should_ignore = false; for (ignore_list) |ignored_cve| { if (strings.eql(vulnerability.id, ignored_cve) or strings.indexOf(vulnerability.url, ignored_cve) != null) { should_ignore = true; break; } } if (should_ignore) { continue; } } if (std.mem.eql(u8, vulnerability.severity, "low")) { vuln_counts.low += 1; } else if (std.mem.eql(u8, vulnerability.severity, "moderate")) { vuln_counts.moderate += 1; } else if (std.mem.eql(u8, vulnerability.severity, "high")) { vuln_counts.high += 1; } else if (std.mem.eql(u8, vulnerability.severity, "critical")) { vuln_counts.critical += 1; } else { vuln_counts.moderate += 1; } try audit_result.all_vulnerabilities.append(vulnerability); } } } } } } } for (audit_result.all_vulnerabilities.items) |vulnerability| { const paths = try findDependencyPaths(allocator, vulnerability.package_name, dependency_tree, pm); const result = try audit_result.vulnerable_packages.getOrPut(vulnerability.package_name); if (!result.found_existing) { result.value_ptr.* = PackageInfo{ .package_id = 0, .name = vulnerability.package_name, .version = vulnerability.vulnerable_versions, .vulnerabilities = std.array_list.Managed(VulnerabilityInfo).init(allocator), .dependents = paths, }; } try result.value_ptr.vulnerabilities.append(vulnerability); } var package_iter = audit_result.vulnerable_packages.iterator(); while (package_iter.next()) |entry| { const package_info = entry.value_ptr; if (package_info.vulnerabilities.items.len > 0) { const main_vuln = package_info.vulnerabilities.items[0]; // const is_direct_dependency: bool = brk: { // for (package_info.dependents.items) |path| { // if (path.is_direct) { // break :brk true; // } // } // break :brk false; // }; if (main_vuln.vulnerable_versions.len > 0) { Output.prettyln("{s} {s}", .{ main_vuln.package_name, main_vuln.vulnerable_versions }); } else { Output.prettyln("{s}", .{main_vuln.package_name}); } for (package_info.dependents.items) |path| { if (path.path.items.len > 1) { if (std.mem.startsWith(u8, path.path.items[0], "workspace:")) { const vulnerable_pkg = path.path.items[path.path.items.len - 1]; const workspace_part = path.path.items[0]; Output.prettyln(" {s} › {s}", .{ workspace_part, vulnerable_pkg }); } else { const vulnerable_pkg = path.path.items[0]; var reversed_items = std.array_list.Managed([]const u8).init(allocator); for (path.path.items[1..]) |item| try reversed_items.append(item); std.mem.reverse([]const u8, reversed_items.items); defer reversed_items.deinit(); const vuln_pkg_path = try std.mem.join(allocator, " › ", reversed_items.items); defer allocator.free(vuln_pkg_path); Output.prettyln(" {s} › {s}", .{ vuln_pkg_path, vulnerable_pkg }); } } else { Output.prettyln(" (direct dependency)", .{}); } } for (package_info.vulnerabilities.items) |vuln| { if (vuln.title.len > 0) { if (std.mem.eql(u8, vuln.severity, "critical")) { Output.prettyln(" critical: {s} - {s}", .{ vuln.title, vuln.url }); } else if (std.mem.eql(u8, vuln.severity, "high")) { Output.prettyln(" high: {s} - {s}", .{ vuln.title, vuln.url }); } else if (std.mem.eql(u8, vuln.severity, "moderate")) { Output.prettyln(" moderate: {s} - {s}", .{ vuln.title, vuln.url }); } else { Output.prettyln(" low: {s} - {s}", .{ vuln.title, vuln.url }); } } } // if (is_direct_dependency) { // Output.prettyln(" To fix: `bun update {s}`", .{package_info.name}); // } else { // Output.prettyln(" To fix: `bun update --latest` (may be a breaking change)", .{}); // } Output.prettyln("", .{}); } } const total = vuln_counts.low + vuln_counts.moderate + vuln_counts.high + vuln_counts.critical; if (total > 0) { Output.pretty("{d} vulnerabilities (", .{total}); var has_previous = false; if (vuln_counts.critical > 0) { Output.pretty("{d} critical", .{vuln_counts.critical}); has_previous = true; } if (vuln_counts.high > 0) { if (has_previous) Output.pretty(", ", .{}); Output.pretty("{d} high", .{vuln_counts.high}); has_previous = true; } if (vuln_counts.moderate > 0) { if (has_previous) Output.pretty(", ", .{}); Output.pretty("{d} moderate", .{vuln_counts.moderate}); has_previous = true; } if (vuln_counts.low > 0) { if (has_previous) Output.pretty(", ", .{}); Output.pretty("{d} low", .{vuln_counts.low}); } Output.prettyln(")", .{}); Output.prettyln("", .{}); Output.prettyln("To update all dependencies to the latest compatible versions:", .{}); Output.prettyln(" bun update", .{}); Output.prettyln("", .{}); Output.prettyln("To update all dependencies to the latest versions (including breaking changes):", .{}); Output.prettyln(" bun update --latest", .{}); Output.prettyln("", .{}); } if (total > 0) { return 1; } } else { Output.writer().writeAll(response_text) catch {}; Output.writer().writeByte('\n') catch {}; } return 0; } const libdeflate = @import("../deps/libdeflate.zig"); const std = @import("std"); const AuditLevel = @import("../install/PackageManager/CommandLineArguments.zig").AuditLevel; const Command = @import("../cli.zig").Command; const PackageManager = @import("../install/install.zig").PackageManager; const URL = @import("../url.zig").URL; const bun = @import("bun"); const Global = bun.Global; const MutableString = bun.MutableString; const Output = bun.Output; const logger = bun.logger; const strings = bun.strings; const http = bun.http; const HeaderBuilder = http.HeaderBuilder;