diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index c29c7b1417..ad77dda303 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -236,6 +236,7 @@ src/ci_info.zig src/cli.zig src/cli/add_command.zig src/cli/add_completions.zig +src/cli/audit_command.zig src/cli/build_command.zig src/cli/bunx_command.zig src/cli/colon_list_type.zig diff --git a/completions/bun.zsh b/completions/bun.zsh index f885ac03ad..9ffaea9012 100644 --- a/completions/bun.zsh +++ b/completions/bun.zsh @@ -260,6 +260,7 @@ _bun_pm_completion() { 'hash\:"generate & print the hash of the current lockfile" ' 'hash-string\:"print the string used to hash the lockfile" ' 'hash-print\:"print the hash stored in the current lockfile" ' + 'audit\:"run a security audit of dependencies in Bun\'s lockfile"' 'cache\:"print the path to the cache folder" ' ) diff --git a/docs/cli/pm.md b/docs/cli/pm.md index 620a08ff9f..51b1807e39 100644 --- a/docs/cli/pm.md +++ b/docs/cli/pm.md @@ -95,6 +95,14 @@ 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/src/cli.zig b/src/cli.zig index ef599daa68..627f1f4407 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -138,6 +138,7 @@ pub const PatchCommitCommand = @import("./cli/patch_commit_command.zig").PatchCo pub const OutdatedCommand = @import("./cli/outdated_command.zig").OutdatedCommand; pub const PublishCommand = @import("./cli/publish_command.zig").PublishCommand; pub const PackCommand = @import("./cli/pack_command.zig").PackCommand; +pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand; pub const InitCommand = @import("./cli/init_command.zig").InitCommand; pub const Arguments = struct { diff --git a/src/cli/audit_command.zig b/src/cli/audit_command.zig new file mode 100644 index 0000000000..8fed3632f2 --- /dev/null +++ b/src/cli/audit_command.zig @@ -0,0 +1,706 @@ +const std = @import("std"); +const bun = @import("bun"); +const Command = @import("../cli.zig").Command; +const PackageManager = @import("../install/install.zig").PackageManager; +const Output = bun.Output; +const Global = bun.Global; +const strings = bun.strings; +const http = bun.http; +const HeaderBuilder = http.HeaderBuilder; +const MutableString = bun.MutableString; +const URL = @import("../url.zig").URL; +const logger = bun.logger; +const semver = @import("../semver.zig"); +const libdeflate = @import("../deps/libdeflate.zig"); + +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.ArrayList(VulnerabilityInfo), + dependents: std.ArrayList(DependencyPath), + + const DependencyPath = struct { + path: std.ArrayList([]const u8), + is_direct: bool, + }; +}; + +const AuditResult = struct { + vulnerable_packages: bun.StringHashMap(PackageInfo), + all_vulnerabilities: std.ArrayList(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.ArrayList(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 { + /// 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), .{}); + 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); + 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 {}; + return 0; + } else if (response_text.len > 0) { + const exit_code = try printEnhancedAuditReport(ctx.allocator, response_text, pm, &dependency_tree); + + 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.ArrayList([]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.ArrayList([]const u8)) { + var dependency_tree = bun.StringHashMap(std.ArrayList([]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.ArrayList([]const u8).init(allocator); + } + try result.value_ptr.append(try allocator.dupe(u8, package_name)); + } + } + + return dependency_tree; +} + +fn collectPackagesForAudit(allocator: std.mem.Allocator, pm: *PackageManager) bun.OOM!struct { audit_body: []u8, skipped_packages: std.ArrayList([]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.ArrayList(struct { + name: []const u8, + versions: std.ArrayList([]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.ArrayList([]const u8).init(allocator); + + 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); + + 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, "{}", .{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.ArrayList([]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); + + 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, + null, + 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.JSAst.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.ArrayList([]const u8)), + pm: *PackageManager, +) bun.OOM!std.ArrayList(PackageInfo.DependencyPath) { + var paths = std.ArrayList(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.ArrayList([]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.ArrayList([]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: std.fifo.LinearFifo([]const u8, .Dynamic) = std.fifo.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.ArrayList([]const u8).init(allocator), + .is_direct = false, + }; + + var trace = current; + while (true) { + try path.path.insert(0, try allocator.dupe(u8, trace.*)); + 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.ArrayList([]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 = @import("../json_parser.zig").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 (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.ArrayList(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.ArrayList([]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; +} diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index 7cb6829ff6..14a97d90e6 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -25,6 +25,7 @@ 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; @@ -131,6 +132,7 @@ pub const PackageManagerCommand = struct { \\ bun pm hash generate & print the hash of the current lockfile \\ bun pm hash-string print the string used to hash the lockfile \\ bun pm hash-print print the hash stored in the current lockfile + \\ bun pm audit check installed packages for vulnerabilities \\ bun pm cache print the path to the cache folder \\ bun pm cache rm clear the cache \\ bun pm migrate migrate another package manager's lockfile without installing anything @@ -250,6 +252,9 @@ 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 99a457ec73..dec5d98b72 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -8963,6 +8963,10 @@ pub const PackageManager = struct { break :brk loader; }; + if (subcommand == .pm and cli.positionals.len >= 2 and strings.eqlComptime(cli.positionals[1], "audit")) { + env.quiet = true; + } + env.loadProcess(); try env.load(entries_option.entries, &[_][]u8{}, .production, false); diff --git a/test/cli/install/__snapshots__/bun-audit.test.ts.snap b/test/cli/install/__snapshots__/bun-audit.test.ts.snap new file mode 100644 index 0000000000..40cb335d30 --- /dev/null +++ b/test/cli/install/__snapshots__/bun-audit.test.ts.snap @@ -0,0 +1,445 @@ +// 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`] = ` +"minimist <0.2.4 + express › mkdirp › minimist + critical: Prototype Pollution in minimist - https://github.com/advisories/GHSA-xvch-5gv4-984h + moderate: Prototype Pollution in minimist - https://github.com/advisories/GHSA-vh95-rmgr-6w4m + +express >=3.4.5 <4.0.0-rc1 + (direct dependency) + low: Express Open Redirect vulnerability - https://github.com/advisories/GHSA-jj78-5fmv-mv28 + low: express vulnerable to XSS via response.redirect() - https://github.com/advisories/GHSA-qw6h-vgh9-j6wx + moderate: Express ressource injection - https://github.com/advisories/GHSA-cm5g-3pgc-8rg4 + moderate: Express.js Open Redirect in malformed URLs - https://github.com/advisories/GHSA-rv95-896h-c2vc + +qs <6.2.4 + express › connect › body-parser › qs + high: qs vulnerable to Prototype Pollution - https://github.com/advisories/GHSA-hrpp-h998-j3pp + high: Prototype Pollution Protection Bypass in qs - https://github.com/advisories/GHSA-gqgv-6jq5-jjj9 + +send <0.19.0 + express › send + low: send vulnerable to template injection that can lead to XSS - https://github.com/advisories/GHSA-m6fv-jmcg-4jfg + +negotiator <0.6.1 + express › connect › serve-index › accepts › negotiator + high: Regular Expression Denial of Service in negotiator - https://github.com/advisories/GHSA-7mc5-chhp-fmc3 + +base64-url <2.0.0 + express › connect › csurf › csrf › uid-safe › base64-url + high: Out-of-bounds Read in base64-url - https://github.com/advisories/GHSA-j4mr-9xw3-c9jx + +serve-static <1.16.0 + express › connect › serve-static + low: serve-static vulnerable to template injection that can lead to XSS - https://github.com/advisories/GHSA-cm22-4g7w-348p + +cookie <0.7.0 + express › connect › cookie + low: cookie accepts cookie name, path, and domain with out of bounds characters - https://github.com/advisories/GHSA-pxg6-pf52-xh8x + +mime <1.4.1 + express › send › mime + high: mime Regular Expression Denial of Service when MIME lookup performed on untrusted user input - https://github.com/advisories/GHSA-wrvr-8mpx-r7pp + +body-parser <1.20.3 + express › connect › body-parser + high: body-parser vulnerable to denial of service when url encoding is enabled - https://github.com/advisories/GHSA-qwcr-r2fm-qrc7 + +fresh <0.5.2 + express › connect › fresh + high: Regular Expression Denial of Service in fresh - https://github.com/advisories/GHSA-9qj9-36jm-prpv + +morgan <1.9.1 + express › connect › morgan + critical: Code Injection in morgan - https://github.com/advisories/GHSA-gwg9-rgvj-4h5j + +basic-auth-connect <1.1.0 + express › connect › basic-auth-connect + high: basic-auth-connect's callback uses time unsafe string comparison - https://github.com/advisories/GHSA-7p89-p6hx-q4fw + +debug <2.6.9 + express › connect › compression › debug + high: debug Inefficient Regular Expression Complexity vulnerability - https://github.com/advisories/GHSA-9vvw-cc9w-f27h + low: Regular Expression Denial of Service in debug - https://github.com/advisories/GHSA-gxpj-cx7g-858c + +ms <2.0.0 + express › connect › serve-favicon › ms + moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability - https://github.com/advisories/GHSA-w9mr-4mfr-499f + +21 vulnerabilities (2 critical, 9 high, 4 moderate, 6 low) + +To update all dependencies to the latest compatible versions: + bun update + +To update all dependencies to the latest versions (including breaking changes): + bun update --latest + +" +`; + +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 pm audit\` should print valid JSON and exit 0 when --json is passed and there are vulnerabilities: bun-audit-expect-valid-json-stdout-report-vulnerabilities 1`] = ` +{ + "base64-url": [ + { + "cvss": { + "score": 0, + "vectorString": null, + }, + "cwe": [ + "CWE-125", + ], + "id": 1090859, + "severity": "high", + "title": "Out-of-bounds Read in base64-url", + "url": "https://github.com/advisories/GHSA-j4mr-9xw3-c9jx", + "vulnerable_versions": "<2.0.0", + }, + ], + "basic-auth-connect": [ + { + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + }, + "cwe": [ + "CWE-208", + ], + "id": 1099800, + "severity": "high", + "title": "basic-auth-connect's callback uses time unsafe string comparison", + "url": "https://github.com/advisories/GHSA-7p89-p6hx-q4fw", + "vulnerable_versions": "<1.1.0", + }, + ], + "body-parser": [ + { + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + }, + "cwe": [ + "CWE-405", + ], + "id": 1099520, + "severity": "high", + "title": "body-parser vulnerable to denial of service when url encoding is enabled", + "url": "https://github.com/advisories/GHSA-qwcr-r2fm-qrc7", + "vulnerable_versions": "<1.20.3", + }, + ], + "cookie": [ + { + "cvss": { + "score": 0, + "vectorString": null, + }, + "cwe": [ + "CWE-74", + ], + "id": 1103907, + "severity": "low", + "title": "cookie accepts cookie name, path, and domain with out of bounds characters", + "url": "https://github.com/advisories/GHSA-pxg6-pf52-xh8x", + "vulnerable_versions": "<0.7.0", + }, + ], + "debug": [ + { + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + }, + "cwe": [ + "CWE-1333", + ], + "id": 1094457, + "severity": "high", + "title": "debug Inefficient Regular Expression Complexity vulnerability", + "url": "https://github.com/advisories/GHSA-9vvw-cc9w-f27h", + "vulnerable_versions": "<2.6.9", + }, + { + "cvss": { + "score": 3.7, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L", + }, + "cwe": [ + "CWE-400", + ], + "id": 1096795, + "severity": "low", + "title": "Regular Expression Denial of Service in debug", + "url": "https://github.com/advisories/GHSA-gxpj-cx7g-858c", + "vulnerable_versions": "<2.6.9", + }, + ], + "express": [ + { + "cvss": { + "score": 4.7, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N", + }, + "cwe": [ + "CWE-601", + ], + "id": 1099969, + "severity": "low", + "title": "Express Open Redirect vulnerability", + "url": "https://github.com/advisories/GHSA-jj78-5fmv-mv28", + "vulnerable_versions": ">=3.4.5 <4.0.0-rc1", + }, + { + "cvss": { + "score": 5, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L", + }, + "cwe": [ + "CWE-79", + ], + "id": 1100530, + "severity": "low", + "title": "express vulnerable to XSS via response.redirect()", + "url": "https://github.com/advisories/GHSA-qw6h-vgh9-j6wx", + "vulnerable_versions": "<4.20.0", + }, + { + "cvss": { + "score": 4, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N", + }, + "cwe": [ + "CWE-74", + ], + "id": 1101381, + "severity": "moderate", + "title": "Express ressource injection", + "url": "https://github.com/advisories/GHSA-cm5g-3pgc-8rg4", + "vulnerable_versions": "<=3.21.4", + }, + { + "cvss": { + "score": 6.1, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + }, + "cwe": [ + "CWE-601", + "CWE-1286", + ], + "id": 1096820, + "severity": "moderate", + "title": "Express.js Open Redirect in malformed URLs", + "url": "https://github.com/advisories/GHSA-rv95-896h-c2vc", + "vulnerable_versions": "<4.19.2", + }, + ], + "fresh": [ + { + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + }, + "cwe": [ + "CWE-400", + ], + "id": 1093570, + "severity": "high", + "title": "Regular Expression Denial of Service in fresh", + "url": "https://github.com/advisories/GHSA-9qj9-36jm-prpv", + "vulnerable_versions": "<0.5.2", + }, + ], + "mime": [ + { + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + }, + "cwe": [ + "CWE-400", + ], + "id": 1093780, + "severity": "high", + "title": "mime Regular Expression Denial of Service when MIME lookup performed on untrusted user input", + "url": "https://github.com/advisories/GHSA-wrvr-8mpx-r7pp", + "vulnerable_versions": "<1.4.1", + }, + ], + "minimist": [ + { + "cvss": { + "score": 9.8, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + }, + "cwe": [ + "CWE-1321", + ], + "id": 1097677, + "severity": "critical", + "title": "Prototype Pollution in minimist", + "url": "https://github.com/advisories/GHSA-xvch-5gv4-984h", + "vulnerable_versions": "<0.2.4", + }, + { + "cvss": { + "score": 5.6, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + }, + "cwe": [ + "CWE-1321", + ], + "id": 1096466, + "severity": "moderate", + "title": "Prototype Pollution in minimist", + "url": "https://github.com/advisories/GHSA-vh95-rmgr-6w4m", + "vulnerable_versions": "<0.2.1", + }, + ], + "morgan": [ + { + "cvss": { + "score": 9.8, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + }, + "cwe": [ + "CWE-94", + ], + "id": 1093804, + "severity": "critical", + "title": "Code Injection in morgan", + "url": "https://github.com/advisories/GHSA-gwg9-rgvj-4h5j", + "vulnerable_versions": "<1.9.1", + }, + ], + "ms": [ + { + "cvss": { + "score": 5.3, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + }, + "cwe": [ + "CWE-1333", + ], + "id": 1094419, + "severity": "moderate", + "title": "Vercel ms Inefficient Regular Expression Complexity vulnerability", + "url": "https://github.com/advisories/GHSA-w9mr-4mfr-499f", + "vulnerable_versions": "<2.0.0", + }, + ], + "negotiator": [ + { + "cvss": { + "score": 0, + "vectorString": null, + }, + "cwe": [ + "CWE-400", + ], + "id": 1090969, + "severity": "high", + "title": "Regular Expression Denial of Service in negotiator", + "url": "https://github.com/advisories/GHSA-7mc5-chhp-fmc3", + "vulnerable_versions": "<0.6.1", + }, + ], + "qs": [ + { + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + }, + "cwe": [ + "CWE-1321", + ], + "id": 1104115, + "severity": "high", + "title": "qs vulnerable to Prototype Pollution", + "url": "https://github.com/advisories/GHSA-hrpp-h998-j3pp", + "vulnerable_versions": "<6.2.4", + }, + { + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + }, + "cwe": [ + "CWE-20", + ], + "id": 1087527, + "severity": "high", + "title": "Prototype Pollution Protection Bypass in qs", + "url": "https://github.com/advisories/GHSA-gqgv-6jq5-jjj9", + "vulnerable_versions": "<6.0.4", + }, + ], + "send": [ + { + "cvss": { + "score": 5, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L", + }, + "cwe": [ + "CWE-79", + ], + "id": 1100526, + "severity": "low", + "title": "send vulnerable to template injection that can lead to XSS", + "url": "https://github.com/advisories/GHSA-m6fv-jmcg-4jfg", + "vulnerable_versions": "<0.19.0", + }, + ], + "serve-static": [ + { + "cvss": { + "score": 5, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L", + }, + "cwe": [ + "CWE-79", + ], + "id": 1100528, + "severity": "low", + "title": "serve-static vulnerable to template injection that can lead to XSS", + "url": "https://github.com/advisories/GHSA-cm22-4g7w-348p", + "vulnerable_versions": "<1.16.0", + }, + ], +} +`; + +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`] = ` +"ms <2.0.0 + (direct dependency) + moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability - https://github.com/advisories/GHSA-w9mr-4mfr-499f + high: Regular Expression Denial of Service in ms - https://github.com/advisories/GHSA-3fx5-fwvr-xrjg + +2 vulnerabilities (1 high, 1 moderate) + +To update all dependencies to the latest compatible versions: + bun update + +To update all dependencies to the latest versions (including breaking changes): + bun update --latest + +" +`; + +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`] = ` +"ms <2.0.0 + (direct dependency) + moderate: Vercel ms Inefficient Regular Expression Complexity vulnerability - https://github.com/advisories/GHSA-w9mr-4mfr-499f + high: Regular Expression Denial of Service in ms - https://github.com/advisories/GHSA-3fx5-fwvr-xrjg + +2 vulnerabilities (1 high, 1 moderate) + +To update all dependencies to the latest compatible versions: + bun update + +To update all dependencies to the latest versions (including breaking changes): + bun update --latest + +" +`; diff --git a/test/cli/install/bun-audit.test.ts b/test/cli/install/bun-audit.test.ts new file mode 100644 index 0000000000..5cce28e51e --- /dev/null +++ b/test/cli/install/bun-audit.test.ts @@ -0,0 +1,290 @@ +import { readableStreamToText, spawn } from "bun"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, DirectoryTree, gunzipJsonRequest, lazyPromiseLike, tempDirWithFiles } from "harness"; +import { join } from "node:path"; +import { resolveBulkAdvisoryFixture } from "./registry/fixtures/audit/audit-fixtures"; + +function fixture( + folder: + | "express@3" + | "vuln-with-only-dev-dependencies" + | "safe-is-number@7" + | "mix-of-safe-and-vulnerable-dependencies", +) { + return join(import.meta.dirname, "registry", "fixtures", "audit", folder); +} + +let server: Bun.Server; + +beforeAll(() => { + server = Bun.serve({ + fetch: async req => { + const body = await gunzipJsonRequest(req); + + const fixture = resolveBulkAdvisoryFixture(body); + + if (!fixture) { + console.log("No fixture found for", body); + return new Response("No fixture found", { status: 404 }); + } + + return Response.json(fixture); + }, + }); +}); + +afterAll(() => { + server.stop(); +}); + +function doAuditTest( + label: string, + options: { + args?: string[]; + exitCode: number; + files: DirectoryTree | string; + fn: (std: { stdout: PromiseLike; stderr: PromiseLike; dir: string }) => Promise; + }, +) { + test(label, async () => { + const dir = tempDirWithFiles("bun-test-pm-audit-" + label.replace(/[^a-zA-Z0-9]/g, "-"), options.files); + + const cmd = [bunExe(), "pm", "audit", ...(options.args ?? [])]; + + const url = server.url.toString().slice(0, -1); + + const proc = spawn({ + cmd, + stdout: "pipe", + stderr: "pipe", + cwd: dir, + env: { + ...bunEnv, + NPM_CONFIG_REGISTRY: url, + }, + }); + + const stdout = lazyPromiseLike(() => readableStreamToText(proc.stdout)); + const stderr = lazyPromiseLike(() => readableStreamToText(proc.stderr)); + + const exitCode = await proc.exited; + + try { + expect(exitCode).toBe(options.exitCode); + await options.fn({ stdout, stderr, dir }); + } catch (e) { + const err = await stderr; + const out = await stdout; + + // useful to see what went wrong otherwise + // we are just eating the rror silently + + console.log("ERR:", err); + console.log("OUT:", out); + + throw e; //but still rethrow so test fails + } + }); +} + +describe("`bun pm audit`", () => { + doAuditTest("should fail with no package.json", { + exitCode: 1, + files: { + "README.md": "This place sure is empty...", + }, + fn: async ({ stderr }) => { + expect(await stderr).toContain("No package.json was found for directory"); + }, + }); + + doAuditTest("should fail with package.json but no lockfile", { + exitCode: 1, + files: { + "package.json": JSON.stringify({ + name: "test", + version: "1.0.0", + dependencies: { + "express": "3", + }, + }), + }, + fn: async ({ stderr }) => { + expect(await stderr).toContain("error: Lockfile not found"); + }, + }); + + doAuditTest("should exit 0 when there are no dependencies in package.json", { + exitCode: 0, + files: { + // i deemed this small enough to justify not needing a fixture + "package.json": JSON.stringify({ + name: "empty-package", + version: "1.0.0", + }), + "bun.lock": JSON.stringify({ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "empty-package", + }, + }, + "packages": {}, + }), + }, + fn: async ({ stdout }) => { + expect(await stdout).toBe("No vulnerabilities found\n"); + }, + }); + + doAuditTest("should exit 0 when there are no vulnerabilities", { + exitCode: 0, + files: fixture("safe-is-number@7"), + fn: async ({ stdout }) => { + expect(await stdout).toBe("No vulnerabilities found\n"); + }, + }); + + doAuditTest("should exit code 1 when there are vulnerabilities", { + exitCode: 1, + files: fixture("express@3"), + fn: async ({ stdout }) => { + expect(await stdout).toMatchSnapshot("bun-audit-expect-vulnerabilities-found"); + }, + }); + + doAuditTest("should print valid JSON and exit 0 when --json is passed and there are no vulnerabilities", { + exitCode: 0, + files: fixture("safe-is-number@7"), + args: ["--json"], + fn: async ({ stdout }) => { + const out = await stdout; + const json = JSON.parse(out); // this would throw making the test fail if the JSON was invalid + expect(json).toMatchSnapshot("bun-audit-expect-valid-json-stdout-report-no-vulnerabilities"); + }, + }); + + doAuditTest("should print valid JSON and exit 0 when --json is passed and there are vulnerabilities", { + exitCode: 0, + files: fixture("express@3"), + args: ["--json"], + fn: async ({ stdout }) => { + const out = await stdout; + const json = JSON.parse(out); // this would throw making the test fail if the JSON was invalid + expect(json).toMatchSnapshot("bun-audit-expect-valid-json-stdout-report-vulnerabilities"); + }, + }); + + doAuditTest( + "should exit 1 and behave exactly the same when there are vulnerabilities when only devDependencies are specified", + { + exitCode: 1, + files: fixture("vuln-with-only-dev-dependencies"), + fn: async ({ stdout }) => { + expect(await stdout).toMatchSnapshot("bun-audit-expect-vulnerabilities-found"); + }, + }, + ); + + doAuditTest( + "when a project has some safe dependencies and some vulnerable dependencies, we should not print the safe dependencies", + { + exitCode: 1, + files: fixture("mix-of-safe-and-vulnerable-dependencies"), + fn: async ({ stdout }) => { + // this fixture is using a safe version of is-number and an unsafe version of ms + // so we want to check that `is-number` is not included in the output and that `ms` is + + const out = await stdout; + + expect(out).toContain("ms"); + expect(out).not.toContain("is-number"); + + expect(out).toMatchSnapshot("bun-audit-expect-vulnerabilities-found"); + }, + }, + ); + + const fakeIntegrity = // this is just random/fake data as the integrity check is not important for this test + "sha512-V8E0l1jyyeSSS9R+J9oljx5eq2rqzClInuwaPcyuv0Mm3ViI/3/rcc4rCEO8i4eQ4I0O0FAGYDA2i5xWHHPhzg=="; + + doAuditTest( + "packages that come from non-default registries should be ignored from the audit, however they should get surfaced at the bottom of the output that they got skipped", + { + exitCode: 0, + files: { + "package.json": JSON.stringify({ + name: "test", + version: "1.0.0", + dependencies: { + "@foo/bar": "1.0.0", + "@foo/baz": "1.0.0", + }, + }), + "bun.lock": JSON.stringify({ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "test", + }, + }, + "packages": { + "@foo/bar": ["@foo/bar@1.0.0", "", {}, fakeIntegrity], + "@foo/baz": ["@foo/baz@1.0.0", "", {}, fakeIntegrity], + }, + }), + //prettier-ignore + ".npmrc": [ + `registry=https://registry.npmjs.org`, + `@foo:registry=https://my-registry.example.com`, + ].join("\n"), + }, + fn: async ({ stdout }) => { + const out = await stdout; + + expect(out).toContain("Skipped @foo/bar, @foo/baz because they do not come from the default registry"); + expect(out).toContain("No vulnerabilities found"); + }, + }, + ); + + doAuditTest("workspaces print the path to the vulnerable package and include workspace:pkg in the name", { + exitCode: 1, + files: { + "package.json": JSON.stringify({ + name: "test", + version: "1.0.0", + workspaces: ["a"], + }), + + "a/package.json": JSON.stringify({ + "name": "a", + "dependencies": { + "ms": "0.7.0", + }, + }), + + "bun.lock": JSON.stringify({ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "bun-audit-playground", + }, + "a": { + "name": "a", + "dependencies": { + "ms": "0.7.0", + }, + }, + }, + "packages": { + "a": ["a@workspace:a"], + "ms": ["ms@0.7.0", "", {}, fakeIntegrity], + }, + }), + }, + fn: async ({ stdout }) => { + expect(await stdout).toInclude("workspace:a › ms"); + }, + }); +}); diff --git a/test/cli/install/registry/fixtures/audit/audit-fixtures.json b/test/cli/install/registry/fixtures/audit/audit-fixtures.json new file mode 100644 index 0000000000..62aeadd551 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/audit-fixtures.json @@ -0,0 +1,397 @@ +{ + "{}": {}, + "{\"ms\":[\"0.7.0\"]}": { + "ms": [ + { + "id": 1094419, + "url": "https://github.com/advisories/GHSA-w9mr-4mfr-499f", + "title": "Vercel ms Inefficient Regular Expression Complexity vulnerability", + "severity": "moderate", + "vulnerable_versions": "<2.0.0", + "cwe": [ + "CWE-1333" + ], + "cvss": { + "score": 5.3, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + }, + { + "id": 1098340, + "url": "https://github.com/advisories/GHSA-3fx5-fwvr-xrjg", + "title": "Regular Expression Denial of Service in ms", + "severity": "high", + "vulnerable_versions": "<0.7.1", + "cwe": [ + "CWE-400", + "CWE-1333" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + } + ] + }, + "{\"is-number\":[\"7.0.0\"],\"ms\":[\"0.7.0\"]}": { + "ms": [ + { + "id": 1094419, + "url": "https://github.com/advisories/GHSA-w9mr-4mfr-499f", + "title": "Vercel ms Inefficient Regular Expression Complexity vulnerability", + "severity": "moderate", + "vulnerable_versions": "<2.0.0", + "cwe": [ + "CWE-1333" + ], + "cvss": { + "score": 5.3, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + }, + { + "id": 1098340, + "url": "https://github.com/advisories/GHSA-3fx5-fwvr-xrjg", + "title": "Regular Expression Denial of Service in ms", + "severity": "high", + "vulnerable_versions": "<0.7.1", + "cwe": [ + "CWE-400", + "CWE-1333" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + } + ] + }, + "{\"is-number\":[\"7.0.0\"]}": {}, + "{\"accepts\":[\"1.2.13\",\"1.3.8\"],\"base64-url\":[\"1.2.1\"],\"basic-auth\":[\"1.0.4\"],\"basic-auth-connect\":[\"1.0.0\"],\"batch\":[\"0.5.3\"],\"body-parser\":[\"1.13.3\"],\"bytes\":[\"2.1.0\",\"2.4.0\"],\"commander\":[\"2.6.0\"],\"compressible\":[\"2.0.18\"],\"compression\":[\"1.5.2\"],\"connect\":[\"2.30.2\"],\"connect-timeout\":[\"1.6.2\"],\"content-disposition\":[\"0.5.0\"],\"content-type\":[\"1.0.5\"],\"cookie\":[\"0.1.3\"],\"cookie-parser\":[\"1.3.5\"],\"cookie-signature\":[\"1.0.6\"],\"core-util-is\":[\"1.0.3\"],\"crc\":[\"3.3.0\"],\"csrf\":[\"3.0.6\"],\"csurf\":[\"1.8.3\"],\"debug\":[\"2.2.0\",\"2.6.9\"],\"depd\":[\"1.0.1\",\"2.0.0\",\"1.1.2\"],\"destroy\":[\"1.0.3\",\"1.0.4\"],\"ee-first\":[\"1.1.1\"],\"errorhandler\":[\"1.4.3\"],\"escape-html\":[\"1.0.2\",\"1.0.3\"],\"etag\":[\"1.7.0\"],\"express\":[\"3.21.2\"],\"express-session\":[\"1.11.3\"],\"finalhandler\":[\"0.4.0\"],\"forwarded\":[\"0.1.2\"],\"fresh\":[\"0.3.0\"],\"http-errors\":[\"1.3.1\"],\"iconv-lite\":[\"0.4.11\",\"0.4.13\"],\"inherits\":[\"2.0.4\"],\"ipaddr.js\":[\"1.0.5\"],\"isarray\":[\"0.0.1\"],\"media-typer\":[\"0.3.0\"],\"merge-descriptors\":[\"1.0.0\"],\"method-override\":[\"2.3.10\"],\"methods\":[\"1.1.2\"],\"mime\":[\"1.3.4\"],\"mime-db\":[\"1.52.0\"],\"mime-types\":[\"2.1.35\"],\"minimist\":[\"0.0.8\"],\"mkdirp\":[\"0.5.1\"],\"morgan\":[\"1.6.1\"],\"ms\":[\"0.7.1\",\"0.7.2\",\"2.0.0\"],\"multiparty\":[\"3.3.2\"],\"negotiator\":[\"0.5.3\",\"0.6.3\"],\"on-finished\":[\"2.3.0\"],\"on-headers\":[\"1.0.2\"],\"parseurl\":[\"1.3.3\"],\"pause\":[\"0.1.0\"],\"proxy-addr\":[\"1.0.10\"],\"qs\":[\"4.0.0\"],\"random-bytes\":[\"1.0.0\"],\"range-parser\":[\"1.0.3\"],\"raw-body\":[\"2.1.7\"],\"readable-stream\":[\"1.1.14\"],\"response-time\":[\"2.3.3\"],\"rndm\":[\"1.2.0\"],\"send\":[\"0.13.0\",\"0.13.2\"],\"serve-favicon\":[\"2.3.2\"],\"serve-index\":[\"1.7.3\"],\"serve-static\":[\"1.10.3\"],\"statuses\":[\"1.2.1\"],\"stream-counter\":[\"0.2.0\"],\"string_decoder\":[\"0.10.31\"],\"tsscmp\":[\"1.0.5\"],\"type-is\":[\"1.6.18\"],\"uid-safe\":[\"2.0.0\",\"2.1.4\"],\"unpipe\":[\"1.0.0\"],\"utils-merge\":[\"1.0.0\"],\"vary\":[\"1.0.1\",\"1.1.2\"],\"vhost\":[\"3.0.2\"]}": { + "base64-url": [ + { + "id": 1090859, + "url": "https://github.com/advisories/GHSA-j4mr-9xw3-c9jx", + "title": "Out-of-bounds Read in base64-url", + "severity": "high", + "vulnerable_versions": "<2.0.0", + "cwe": [ + "CWE-125" + ], + "cvss": { + "score": 0, + "vectorString": null + } + } + ], + "basic-auth-connect": [ + { + "id": 1099800, + "url": "https://github.com/advisories/GHSA-7p89-p6hx-q4fw", + "title": "basic-auth-connect's callback uses time unsafe string comparison", + "severity": "high", + "vulnerable_versions": "<1.1.0", + "cwe": [ + "CWE-208" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" + } + } + ], + "body-parser": [ + { + "id": 1099520, + "url": "https://github.com/advisories/GHSA-qwcr-r2fm-qrc7", + "title": "body-parser vulnerable to denial of service when url encoding is enabled", + "severity": "high", + "vulnerable_versions": "<1.20.3", + "cwe": [ + "CWE-405" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + } + ], + "cookie": [ + { + "id": 1103907, + "url": "https://github.com/advisories/GHSA-pxg6-pf52-xh8x", + "title": "cookie accepts cookie name, path, and domain with out of bounds characters", + "severity": "low", + "vulnerable_versions": "<0.7.0", + "cwe": [ + "CWE-74" + ], + "cvss": { + "score": 0, + "vectorString": null + } + } + ], + "debug": [ + { + "id": 1094457, + "url": "https://github.com/advisories/GHSA-9vvw-cc9w-f27h", + "title": "debug Inefficient Regular Expression Complexity vulnerability", + "severity": "high", + "vulnerable_versions": "<2.6.9", + "cwe": [ + "CWE-1333" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + }, + { + "id": 1096795, + "url": "https://github.com/advisories/GHSA-gxpj-cx7g-858c", + "title": "Regular Expression Denial of Service in debug", + "severity": "low", + "vulnerable_versions": "<2.6.9", + "cwe": [ + "CWE-400" + ], + "cvss": { + "score": 3.7, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + } + ], + "express": [ + { + "id": 1099969, + "url": "https://github.com/advisories/GHSA-jj78-5fmv-mv28", + "title": "Express Open Redirect vulnerability", + "severity": "low", + "vulnerable_versions": ">=3.4.5 <4.0.0-rc1", + "cwe": [ + "CWE-601" + ], + "cvss": { + "score": 4.7, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N" + } + }, + { + "id": 1100530, + "url": "https://github.com/advisories/GHSA-qw6h-vgh9-j6wx", + "title": "express vulnerable to XSS via response.redirect()", + "severity": "low", + "vulnerable_versions": "<4.20.0", + "cwe": [ + "CWE-79" + ], + "cvss": { + "score": 5, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L" + } + }, + { + "id": 1101381, + "url": "https://github.com/advisories/GHSA-cm5g-3pgc-8rg4", + "title": "Express ressource injection", + "severity": "moderate", + "vulnerable_versions": "<=3.21.4", + "cwe": [ + "CWE-74" + ], + "cvss": { + "score": 4, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N" + } + }, + { + "id": 1096820, + "url": "https://github.com/advisories/GHSA-rv95-896h-c2vc", + "title": "Express.js Open Redirect in malformed URLs", + "severity": "moderate", + "vulnerable_versions": "<4.19.2", + "cwe": [ + "CWE-601", + "CWE-1286" + ], + "cvss": { + "score": 6.1, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N" + } + } + ], + "fresh": [ + { + "id": 1093570, + "url": "https://github.com/advisories/GHSA-9qj9-36jm-prpv", + "title": "Regular Expression Denial of Service in fresh", + "severity": "high", + "vulnerable_versions": "<0.5.2", + "cwe": [ + "CWE-400" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + } + ], + "mime": [ + { + "id": 1093780, + "url": "https://github.com/advisories/GHSA-wrvr-8mpx-r7pp", + "title": "mime Regular Expression Denial of Service when MIME lookup performed on untrusted user input", + "severity": "high", + "vulnerable_versions": "<1.4.1", + "cwe": [ + "CWE-400" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + } + ], + "minimist": [ + { + "id": 1097677, + "url": "https://github.com/advisories/GHSA-xvch-5gv4-984h", + "title": "Prototype Pollution in minimist", + "severity": "critical", + "vulnerable_versions": "<0.2.4", + "cwe": [ + "CWE-1321" + ], + "cvss": { + "score": 9.8, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + }, + { + "id": 1096466, + "url": "https://github.com/advisories/GHSA-vh95-rmgr-6w4m", + "title": "Prototype Pollution in minimist", + "severity": "moderate", + "vulnerable_versions": "<0.2.1", + "cwe": [ + "CWE-1321" + ], + "cvss": { + "score": 5.6, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L" + } + } + ], + "morgan": [ + { + "id": 1093804, + "url": "https://github.com/advisories/GHSA-gwg9-rgvj-4h5j", + "title": "Code Injection in morgan", + "severity": "critical", + "vulnerable_versions": "<1.9.1", + "cwe": [ + "CWE-94" + ], + "cvss": { + "score": 9.8, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + } + ], + "ms": [ + { + "id": 1094419, + "url": "https://github.com/advisories/GHSA-w9mr-4mfr-499f", + "title": "Vercel ms Inefficient Regular Expression Complexity vulnerability", + "severity": "moderate", + "vulnerable_versions": "<2.0.0", + "cwe": [ + "CWE-1333" + ], + "cvss": { + "score": 5.3, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + } + ], + "negotiator": [ + { + "id": 1090969, + "url": "https://github.com/advisories/GHSA-7mc5-chhp-fmc3", + "title": "Regular Expression Denial of Service in negotiator", + "severity": "high", + "vulnerable_versions": "<0.6.1", + "cwe": [ + "CWE-400" + ], + "cvss": { + "score": 0, + "vectorString": null + } + } + ], + "qs": [ + { + "id": 1104115, + "url": "https://github.com/advisories/GHSA-hrpp-h998-j3pp", + "title": "qs vulnerable to Prototype Pollution", + "severity": "high", + "vulnerable_versions": "<6.2.4", + "cwe": [ + "CWE-1321" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + }, + { + "id": 1087527, + "url": "https://github.com/advisories/GHSA-gqgv-6jq5-jjj9", + "title": "Prototype Pollution Protection Bypass in qs", + "severity": "high", + "vulnerable_versions": "<6.0.4", + "cwe": [ + "CWE-20" + ], + "cvss": { + "score": 7.5, + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + } + ], + "send": [ + { + "id": 1100526, + "url": "https://github.com/advisories/GHSA-m6fv-jmcg-4jfg", + "title": "send vulnerable to template injection that can lead to XSS", + "severity": "low", + "vulnerable_versions": "<0.19.0", + "cwe": [ + "CWE-79" + ], + "cvss": { + "score": 5, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L" + } + } + ], + "serve-static": [ + { + "id": 1100528, + "url": "https://github.com/advisories/GHSA-cm22-4g7w-348p", + "title": "serve-static vulnerable to template injection that can lead to XSS", + "severity": "low", + "vulnerable_versions": "<1.16.0", + "cwe": [ + "CWE-79" + ], + "cvss": { + "score": 5, + "vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L" + } + } + ] + } +} \ No newline at end of file diff --git a/test/cli/install/registry/fixtures/audit/audit-fixtures.ts b/test/cli/install/registry/fixtures/audit/audit-fixtures.ts new file mode 100644 index 0000000000..4afa092d1c --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/audit-fixtures.ts @@ -0,0 +1,46 @@ +import auditFixturesJson from "./audit-fixtures.json" with { type: "json" }; + +type AuditReport = (typeof auditFixturesJson)[keyof typeof auditFixturesJson]; + +const fixtures = Object.entries(auditFixturesJson).map( + ([key, value]) => [JSON.parse(key) as Record, value as AuditReport] as const, +); + +export function resolveBulkAdvisoryFixture(request: Record) { + for (const [body, response] of fixtures) { + if (isSameJSON(body, request)) { + return response; + } + } + + return undefined; +} + +type JsonValue = string | number | boolean | null | JsonValue[] | { [key in string]: JsonValue }; + +function isSameJSON(a: T, b: T) { + return sortedObjectHash(a) === sortedObjectHash(b); +} + +function sortedObjectHash(obj: JsonValue): string { + if (typeof obj === "string") { + return JSON.stringify(obj); + } + + if (Array.isArray(obj)) { + const elements = obj.map(sortedObjectHash); + return `[${elements.join(",")}]`; + } + + if (typeof obj === "object" && obj !== null) { + const sortedKeys = Object.keys(obj).sort(); + const pairs = sortedKeys.map(key => `${JSON.stringify(key)}:${sortedObjectHash(obj[key])}`); + return `{${pairs.join(",")}}`; + } + + if (typeof obj === "number" || typeof obj === "boolean" || obj === null) { + return String(obj); + } + + return obj satisfies never; +} diff --git a/test/cli/install/registry/fixtures/audit/express@3/bun.lock b/test/cli/install/registry/fixtures/audit/express@3/bun.lock new file mode 100644 index 0000000000..13fd458a21 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/express@3/bun.lock @@ -0,0 +1,198 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "express-3", + "dependencies": { + "express": "3", + }, + }, + }, + "packages": { + "accepts": ["accepts@1.2.13", "", { "dependencies": { "mime-types": "~2.1.6", "negotiator": "0.5.3" } }, "sha512-R190A3EzrS4huFOVZajhXCYZt5p5yrkaQOB4nsWzfth0cYaDcSN5J86l58FJ1dt7igp37fB/QhnuFkGAJmr+eg=="], + + "base64-url": ["base64-url@1.2.1", "", {}, "sha512-V8E0l1jyyeSSS9R+J9oljx5eq2rqzClInuwaPcyuv0Mm3ViI/3/rcc4rCEO8i4eQ4I0O0FAGYDA2i5xWHHPhzg=="], + + "basic-auth": ["basic-auth@1.0.4", "", {}, "sha512-uvq3I/zC5TmG0WZJDzsXzIytU9GiiSq23Gl27Dq9sV81JTfPfQhtdADECP1DJZeJoZPuYU0Y81hWC5y/dOR+Yw=="], + + "basic-auth-connect": ["basic-auth-connect@1.0.0", "", {}, "sha512-kiV+/DTgVro4aZifY/hwRwALBISViL5NP4aReaR2EVJEObpbUBHIkdJh/YpcoEiYt7nBodZ6U2ajZeZvSxUCCg=="], + + "batch": ["batch@0.5.3", "", {}, "sha512-aQgHPLH2DHpFTpBl5/GiVdNzHEqsLCSs1RiPvqkKP1+7RkNJlv71kL8/KXmvvaLqoZ7ylmvqkZhLjjAoRz8Xgw=="], + + "body-parser": ["body-parser@1.13.3", "", { "dependencies": { "bytes": "2.1.0", "content-type": "~1.0.1", "debug": "~2.2.0", "depd": "~1.0.1", "http-errors": "~1.3.1", "iconv-lite": "0.4.11", "on-finished": "~2.3.0", "qs": "4.0.0", "raw-body": "~2.1.2", "type-is": "~1.6.6" } }, "sha512-ypX8/9uws2W+CjPp3QMmz1qklzlhRBknQve22Y+WFecHql+qDFfG+VVNX7sooA4Q3+2fdq4ZZj6Xr07gA90RZg=="], + + "bytes": ["bytes@2.1.0", "", {}, "sha512-k9VSlRfRi5JYyQWMylSOgjld96ta1qaQUIvmn+na0BzViclH04PBumewv4z5aeXNkn6Z/gAN5FtPeBLvV20F9w=="], + + "commander": ["commander@2.6.0", "", {}, "sha512-PhbTMT+ilDXZKqH8xbvuUY2ZEQNef0Q7DKxgoEKb4ccytsdvVVJmYqR0sGbi96nxU6oGrwEIQnclpK2NBZuQlg=="], + + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], + + "compression": ["compression@1.5.2", "", { "dependencies": { "accepts": "~1.2.12", "bytes": "2.1.0", "compressible": "~2.0.5", "debug": "~2.2.0", "on-headers": "~1.0.0", "vary": "~1.0.1" } }, "sha512-+2fE8M8+Oe0kAlbMPz6UinaaH/HaGf+c5HlWRyYtPga/PHKxStJJKTU4xca8StY0JQ78L2kJaslpgSzCKgHaxQ=="], + + "connect": ["connect@2.30.2", "", { "dependencies": { "basic-auth-connect": "1.0.0", "body-parser": "~1.13.3", "bytes": "2.1.0", "compression": "~1.5.2", "connect-timeout": "~1.6.2", "content-type": "~1.0.1", "cookie": "0.1.3", "cookie-parser": "~1.3.5", "cookie-signature": "1.0.6", "csurf": "~1.8.3", "debug": "~2.2.0", "depd": "~1.0.1", "errorhandler": "~1.4.2", "express-session": "~1.11.3", "finalhandler": "0.4.0", "fresh": "0.3.0", "http-errors": "~1.3.1", "method-override": "~2.3.5", "morgan": "~1.6.1", "multiparty": "3.3.2", "on-headers": "~1.0.0", "parseurl": "~1.3.0", "pause": "0.1.0", "qs": "4.0.0", "response-time": "~2.3.1", "serve-favicon": "~2.3.0", "serve-index": "~1.7.2", "serve-static": "~1.10.0", "type-is": "~1.6.6", "utils-merge": "1.0.0", "vhost": "~3.0.1" } }, "sha512-eY4YHls5bz/g6h9Q8B/aVkS6D7+TRiRlI3ksuruv3yc2rLbTG7HB/7T/CoZsuVH5e2i3S9J+2eARV5o7GIYq8Q=="], + + "connect-timeout": ["connect-timeout@1.6.2", "", { "dependencies": { "debug": "~2.2.0", "http-errors": "~1.3.1", "ms": "0.7.1", "on-headers": "~1.0.0" } }, "sha512-qIFt3Ja6gRuJtVoWhPa5FtOO8ERs0MfW/QkmQ0vjrAL78otrkxe8w/qjTAgU/T1W/jH5qeZXJHilmOPKNTiEQw=="], + + "content-disposition": ["content-disposition@0.5.0", "", {}, "sha512-PWzG8GssMHTPSLBoOeK5MvPPJeWU5ZVX8omvJC16BUH/nUX6J/jM/hgm/mrPWzTXVV3B3OoBhFdHXyGLU4TgUw=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.1.3", "", {}, "sha512-mWkFhcL+HVG1KjeCjEBVJJ7s4sAGMLiBDFSDs4bzzvgLZt7rW8BhP6XV/8b1+pNvx/skd3yYxPuaF3Z6LlQzyw=="], + + "cookie-parser": ["cookie-parser@1.3.5", "", { "dependencies": { "cookie": "0.1.3", "cookie-signature": "1.0.6" } }, "sha512-YN/8nzPcK5o6Op4MIzAd4H4qUal5+3UaMhVIeaafFYL0pKvBQA/9Yhzo7ZwvBpjdGshsiTAb1+FC37M6RdPDFg=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "crc": ["crc@3.3.0", "", {}, "sha512-QCx3z7FOZbJrapsnewTkh1Hxh6PHV61SRHbx6Q65Uih3y0kfIj+dDGI3uQ4Q1DLKOILyvpZxvJpoKPrxathpCg=="], + + "csrf": ["csrf@3.0.6", "", { "dependencies": { "rndm": "1.2.0", "tsscmp": "1.0.5", "uid-safe": "2.1.4" } }, "sha512-3q1ocniLMgk9nHHEt/I/JsN9IfiGjgp6MHgYNT7+CPmQvi5DF6qzenXnZSH6f9Qaa+4DhmUDJa8SgFZ+OFf9Qg=="], + + "csurf": ["csurf@1.8.3", "", { "dependencies": { "cookie": "0.1.3", "cookie-signature": "1.0.6", "csrf": "~3.0.0", "http-errors": "~1.3.1" } }, "sha512-p2NJ9fGOn5HCaV9jAOBCSjIGMRMrpm9/yDswD0bFi7zQv1ifDufIKI5nem9RmhMsH6jVD6Sx6vs57hnivvkJJw=="], + + "debug": ["debug@2.2.0", "", { "dependencies": { "ms": "0.7.1" } }, "sha512-X0rGvJcskG1c3TgSCPqHJ0XJgwlcvOC7elJ5Y0hYuKBZoVqWpAMfLOeIh2UI/DCQ5ruodIjvsugZtjUYUw2pUw=="], + + "depd": ["depd@1.0.1", "", {}, "sha512-OEWAMbCkK9IWQ8pfTvHBhCSqHgR+sk5pbiYqq0FqfARG4Cy+cRsCbITx6wh5pcsmfBPiJAcbd98tfdz5fnBbag=="], + + "destroy": ["destroy@1.0.3", "", {}, "sha512-KB/AVLKRwZPOEo6/lxkDJ+Bv3jFRRrhmnRMPvpWwmIfUggpzGkQBqolyo8FRf833b/F5rzmy1uVN3fHBkjTxgw=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "errorhandler": ["errorhandler@1.4.3", "", { "dependencies": { "accepts": "~1.3.0", "escape-html": "~1.0.3" } }, "sha512-pp1hk9sZBq4Bj/e/Cl84fJ3cYiQDFZk3prp7jrurUbPGOlY7zA2OubjhhEAWuUb8VNTFIkGwoby7Uq6YpicfvQ=="], + + "escape-html": ["escape-html@1.0.2", "", {}, "sha512-J5ahyCRC4liskWVAfkmosNWfG0eHQxI0W+Ko7k3cZaYVMfgt05dwZ68vw6S/TZM1BPvuTv3kq6CRCb7WWtBUVA=="], + + "etag": ["etag@1.7.0", "", {}, "sha512-Mbv5pNpLNPrm1b4rzZlZlfTRpdDr31oiD43N362sIyvSWVNu5Du33EcJGzvEV4YdYLuENB1HzND907cQkFmXNw=="], + + "express": ["express@3.21.2", "", { "dependencies": { "basic-auth": "~1.0.3", "commander": "2.6.0", "connect": "2.30.2", "content-disposition": "0.5.0", "content-type": "~1.0.1", "cookie": "0.1.3", "cookie-signature": "1.0.6", "debug": "~2.2.0", "depd": "~1.0.1", "escape-html": "1.0.2", "etag": "~1.7.0", "fresh": "0.3.0", "merge-descriptors": "1.0.0", "methods": "~1.1.1", "mkdirp": "0.5.1", "parseurl": "~1.3.0", "proxy-addr": "~1.0.8", "range-parser": "~1.0.2", "send": "0.13.0", "utils-merge": "1.0.0", "vary": "~1.0.1" }, "bin": { "express": "./bin/express" } }, "sha512-r3mq2RNCDxAdmZrzEAdjlk5/W7x8+vjU1aAcoAoZFq62KtkWQX+MbaSN4g59CwdUFf9MFf1VSqkZJ+LeR9jmww=="], + + "express-session": ["express-session@1.11.3", "", { "dependencies": { "cookie": "0.1.3", "cookie-signature": "1.0.6", "crc": "3.3.0", "debug": "~2.2.0", "depd": "~1.0.1", "on-headers": "~1.0.0", "parseurl": "~1.3.0", "uid-safe": "~2.0.0", "utils-merge": "1.0.0" } }, "sha512-QdSbGRRg+JMvlYpancRDFXDmIMqjEdpowriwQc4Kz3mvPwTnOPD/h5FSS21+4z4Isosta+ULmEwL6F3/lylWWg=="], + + "finalhandler": ["finalhandler@0.4.0", "", { "dependencies": { "debug": "~2.2.0", "escape-html": "1.0.2", "on-finished": "~2.3.0", "unpipe": "~1.0.0" } }, "sha512-jJU2WE88OqUvwAIf/1K2G2fTdKKZ8LvSwYQyFFekDcmBnBmht38enbcmErnA7iNZktcEo/o2JAHYbe1QDOAgaA=="], + + "forwarded": ["forwarded@0.1.2", "", {}, "sha512-Ua9xNhH0b8pwE3yRbFfXJvfdWF0UHNCdeyb2sbi9Ul/M+r3PTdrz7Cv4SCfZRMjmzEM9PhraqfZFbGTIg3OMyA=="], + + "fresh": ["fresh@0.3.0", "", {}, "sha512-akx5WBKAwMSg36qoHTuMMVncHWctlaDGslJASDYAhoLrzDUDCjZlOngNa/iC6lPm9aA0qk8pN5KnpmbJHSIIQQ=="], + + "http-errors": ["http-errors@1.3.1", "", { "dependencies": { "inherits": "~2.0.1", "statuses": "1" } }, "sha512-gMygNskMurDCWfoCdyh1gOeDfSbkAHXqz94QoPj5IHIUjC/BG8/xv7FSEUr7waR5RcAya4j58bft9Wu/wHNeXA=="], + + "iconv-lite": ["iconv-lite@0.4.11", "", {}, "sha512-8UmnaYeP5puk18SkBrYULVTiq7REcimhx+ykJVJBiaz89DQmVQAfS29ZhHah86la90/t0xy4vRk86/2cCwNodA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.0.5", "", {}, "sha512-wBj+q+3uP78gMowwWgFLAYm/q4x5goyZmDsmuvyz+nd1u0D/ghgXXtc1OkgmTzSiWT101kiqGacwFk9eGQw6xQ=="], + + "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.0", "", {}, "sha512-YJiZmTZTkrqvgefMsWdioTKsZdHnfAhHHkEdPg+4PCqMJEGHQo5iJQjEbMv3XyBZ6y3Z2Rj1mqq1WNKq9e0yNw=="], + + "method-override": ["method-override@2.3.10", "", { "dependencies": { "debug": "2.6.9", "methods": "~1.1.2", "parseurl": "~1.3.2", "vary": "~1.1.2" } }, "sha512-Ks2/7e+3JuwQcpLybc6wTHyqg13HDjOhLcE+YaAEub9DbSxF+ieMvxUlybmWW9luRMh9Cd0rO9aNtzUT51xfNQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.3.4", "", { "bin": { "mime": "cli.js" } }, "sha512-sAaYXszED5ALBt665F0wMQCUXpGuZsGdopoqcHPdL39ZYdi7uHoZlhrfZfhv8WzivhBzr/oXwaj+yiK5wY8MXQ=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimist": ["minimist@0.0.8", "", {}, "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q=="], + + "mkdirp": ["mkdirp@0.5.1", "", { "dependencies": { "minimist": "0.0.8" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA=="], + + "morgan": ["morgan@1.6.1", "", { "dependencies": { "basic-auth": "~1.0.3", "debug": "~2.2.0", "depd": "~1.0.1", "on-finished": "~2.3.0", "on-headers": "~1.0.0" } }, "sha512-WWxlTx5xCqbtSeX/gPVHUZBhAhSMfYQLgPrWHEN0FYnF+zf1Ju/Zct6rpeKmvzibrYF4QvFVws7IN61BxnKu+Q=="], + + "ms": ["ms@0.7.1", "", {}, "sha512-lRLiIR9fSNpnP6TC4v8+4OU7oStC01esuNowdQ34L+Gk8e5Puoc88IqJ+XAY/B3Mn2ZKis8l8HX90oU8ivzUHg=="], + + "multiparty": ["multiparty@3.3.2", "", { "dependencies": { "readable-stream": "~1.1.9", "stream-counter": "~0.2.0" } }, "sha512-FX6dDOKzDpkrb5/+Imq+V6dmCZNnC02tMDiZfrgHSYgfQj6CVPGzOVqfbHKt/Vy4ZZsmMPXkulyLf92lCyvV7A=="], + + "negotiator": ["negotiator@0.5.3", "", {}, "sha512-oXmnazqehLNFohqgLxRyUdOQU9/UX0NpCpsnbjWUjM62ZM8oSOXYZpHc68XR130ftPNano0oQXGdREAplZRhaQ=="], + + "on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], + + "on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "pause": ["pause@0.1.0", "", {}, "sha512-aeHLgQCtI3tcuYVnrvAeVb4Tkm1za4r3YDv3hMeUxcRxet3dbEhJOdtoMrsT/Q5tY3Oy2A1A9FD5el5tWp2FSg=="], + + "proxy-addr": ["proxy-addr@1.0.10", "", { "dependencies": { "forwarded": "~0.1.0", "ipaddr.js": "1.0.5" } }, "sha512-iq6kR9KN32aFvXjDyC8nIrm203AHeIBPjL6dpaHgSdbpTO8KoPlD0xG92xwwtkCL9+yt1LE5VwpEk43TyP38Dg=="], + + "qs": ["qs@4.0.0", "", {}, "sha512-8MPmJ83uBOPsQj5tQCv4g04/nTiY+d17yl9o3Bw73vC6XlEm2POIRRlOgWJ8i74bkGLII670cDJJZkgiZ2sIkg=="], + + "random-bytes": ["random-bytes@1.0.0", "", {}, "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="], + + "range-parser": ["range-parser@1.0.3", "", {}, "sha512-nDsRrtIxVUO5opg/A8T2S3ebULVIfuh8ECbh4w3N4mWxIiT3QILDJDUQayPqm2e8Q8NUa0RSUkGCfe33AfjR3Q=="], + + "raw-body": ["raw-body@2.1.7", "", { "dependencies": { "bytes": "2.4.0", "iconv-lite": "0.4.13", "unpipe": "1.0.0" } }, "sha512-x4d27vsIG04gZ1imkuDXB9Rd/EkAx5kYzeMijIYw1PAor0Ld3nTlkQQwDjKu42GdRUFCX1AfGnTSQB4O57eWVg=="], + + "readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="], + + "response-time": ["response-time@2.3.3", "", { "dependencies": { "depd": "~2.0.0", "on-headers": "~1.0.1" } }, "sha512-SsjjOPHl/FfrTQNgmc5oen8Hr1Jxpn6LlHNXxCIFdYMHuK1kMeYMobb9XN3mvxaGQm3dbegqYFMX4+GDORfbWg=="], + + "rndm": ["rndm@1.2.0", "", {}, "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw=="], + + "send": ["send@0.13.0", "", { "dependencies": { "debug": "~2.2.0", "depd": "~1.0.1", "destroy": "1.0.3", "escape-html": "1.0.2", "etag": "~1.7.0", "fresh": "0.3.0", "http-errors": "~1.3.1", "mime": "1.3.4", "ms": "0.7.1", "on-finished": "~2.3.0", "range-parser": "~1.0.2", "statuses": "~1.2.1" } }, "sha512-zck2y84i0SbUUiwq2l5gGPNVpCplL48og5xIhFjNjQa09003YCTy6Vb3rKfVuG8W8PWNUtUOntjQEBdwkJ9oBw=="], + + "serve-favicon": ["serve-favicon@2.3.2", "", { "dependencies": { "etag": "~1.7.0", "fresh": "0.3.0", "ms": "0.7.2", "parseurl": "~1.3.1" } }, "sha512-oHEaA3ohvKxEWhjP97cQ6QuTTbMBF3AxDyMSvBtvnl1jXaB2Ik6kXE7nUtPM3YVU5VHCDe6n7JZrFCWzQuvXEQ=="], + + "serve-index": ["serve-index@1.7.3", "", { "dependencies": { "accepts": "~1.2.13", "batch": "0.5.3", "debug": "~2.2.0", "escape-html": "~1.0.3", "http-errors": "~1.3.1", "mime-types": "~2.1.9", "parseurl": "~1.3.1" } }, "sha512-g18EQWY83uFBldFpCyK/a49yxQgIMEMLA6U9f66FiI848mLkMO8EY/xRAZAoCwNFwSUAiArCF3mdjaNXpd3ghw=="], + + "serve-static": ["serve-static@1.10.3", "", { "dependencies": { "escape-html": "~1.0.3", "parseurl": "~1.3.1", "send": "0.13.2" } }, "sha512-ScsFovjz3Db+vGgpofR/U8p8UULEcGV9akqyo8TQ1mMnjcxemE7Y5Muo+dvy3tJLY/doY2v1H61eCBMYGmwfrA=="], + + "statuses": ["statuses@1.2.1", "", {}, "sha512-pVEuxHdSGrt8QmQ3LOZXLhSA6MP/iPqKzZeO6Squ7PNGkA/9MBsSfV0/L+bIxkoDmjF4tZcLpcVq/fkqoHvuKg=="], + + "stream-counter": ["stream-counter@0.2.0", "", { "dependencies": { "readable-stream": "~1.1.8" } }, "sha512-GjA2zKc2iXUUKRcOxXQmhEx0Ev3XHJ6c8yWGqhQjWwhGrqNwSsvq9YlRLgoGtZ5Kx2Ln94IedaqJ5GUG6aBbxA=="], + + "string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="], + + "tsscmp": ["tsscmp@1.0.5", "", {}, "sha512-aP/vy9xYiYGvtpW4xBkxdoeqbT+nNeo/37cdQk3iSiGz0xKb20XwOgBSqYo1DzEqt1ycPubEfPU3oHgzsRRL3g=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "uid-safe": ["uid-safe@2.0.0", "", { "dependencies": { "base64-url": "1.2.1" } }, "sha512-PH/12q0a/sEGVS28fZ5evILW2Ayn13PwkYmCleDsIPm39vUIqN58hjyqtUd496kyMY6WkXtaDMDpS8nSCmNKTg=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "utils-merge": ["utils-merge@1.0.0", "", {}, "sha512-HwU9SLQEtyo+0uoKXd1nkLqigUWLB+QuNQR4OcmB73eWqksM5ovuqcycks2x043W8XVb75rG1HQ0h93TMXkzQQ=="], + + "vary": ["vary@1.0.1", "", {}, "sha512-yNsH+tC0r8quK2tg/yqkXqqaYzeKTkSqQ+8T6xCoWgOi/bU/omMYz+6k+I91JJJDeltJzI7oridTOq6OYkY0Tw=="], + + "vhost": ["vhost@3.0.2", "", {}, "sha512-S3pJdWrpFWrKMboRU4dLYgMrTgoPALsmYwOvyebK2M6X95b9kQrjZy5rwl3uzzpfpENe/XrNYu/2U+e7/bmT5g=="], + + "csrf/uid-safe": ["uid-safe@2.1.4", "", { "dependencies": { "random-bytes": "~1.0.0" } }, "sha512-MHTGzIDNPv1XhDK0MyKvEroobUhtpMa649/9SIFbTRO2dshLctD3zxOwQw+gQ+Mlp5osfMdUU1sjcO6Fw4rvCA=="], + + "errorhandler/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "errorhandler/escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "method-override/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "method-override/vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "raw-body/bytes": ["bytes@2.4.0", "", {}, "sha512-SvUX8+c/Ga454a4fprIdIUzUN9xfd1YTvYh7ub5ZPJ+ZJ/+K2Bp6IpWGmnw8r3caLTsmhvJAKZz3qjIo9+XuCQ=="], + + "raw-body/iconv-lite": ["iconv-lite@0.4.13", "", {}, "sha512-QwVuTNQv7tXC5mMWFX5N5wGjmybjNBBD8P3BReTkPmipoxTUFgWM2gXNvldHQr6T14DH0Dh6qBVg98iJt7u4mQ=="], + + "response-time/depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "serve-favicon/ms": ["ms@0.7.2", "", {}, "sha512-5NnE67nQSQDJHVahPJna1PQ/zCXMnQop3yUCxjKPNzCxuyPSKWTQ/5Gu5CZmjetwGLWRA+PzeF5thlbOdbQldA=="], + + "serve-index/escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "serve-static/escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "serve-static/send": ["send@0.13.2", "", { "dependencies": { "debug": "~2.2.0", "depd": "~1.1.0", "destroy": "~1.0.4", "escape-html": "~1.0.3", "etag": "~1.7.0", "fresh": "0.3.0", "http-errors": "~1.3.1", "mime": "1.3.4", "ms": "0.7.1", "on-finished": "~2.3.0", "range-parser": "~1.0.3", "statuses": "~1.2.1" } }, "sha512-cQ0rmXHrdO2Iof08igV2bG/yXWD106ANwBg6DkGQNT2Vsznbgq6T0oAIQboy1GoFsIuy51jCim26aA9tj3Z3Zg=="], + + "errorhandler/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "method-override/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "serve-static/send/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], + + "serve-static/send/destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="], + } +} diff --git a/test/cli/install/registry/fixtures/audit/express@3/package.json b/test/cli/install/registry/fixtures/audit/express@3/package.json new file mode 100644 index 0000000000..a0650bd7f4 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/express@3/package.json @@ -0,0 +1,6 @@ +{ + "name": "express-3", + "dependencies": { + "express": "3" + } +} diff --git a/test/cli/install/registry/fixtures/audit/generate-audit-fixtures.ts b/test/cli/install/registry/fixtures/audit/generate-audit-fixtures.ts new file mode 100644 index 0000000000..34cb1e4916 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/generate-audit-fixtures.ts @@ -0,0 +1,67 @@ +import { $, readableStreamToText, spawn } from "bun"; +import { bunEnv, bunExe, gunzipJsonRequest, tempDirWithFiles } from "harness"; +import * as path from "node:path"; + +const output = path.join(import.meta.dirname, "audit-fixtures.json"); + +const packages = await Array.fromAsync( + new Bun.Glob("./*/package.json").scan({ + cwd: import.meta.dirname, + }), +); + +const absolutes = packages.map(p => path.resolve(import.meta.dirname, p)); + +const result: Record = { + "{}": {}, +}; + +for (const packageJsonPath of absolutes) { + const directory = path.dirname(packageJsonPath); + const tmp = tempDirWithFiles("bun-audit-fixture-generator", directory); + + const { promise: requestBodyPromise, resolve, reject } = Promise.withResolvers(); + + using server = Bun.serve({ + port: 12345, + fetch: async req => { + try { + const body = await gunzipJsonRequest(req); + resolve(JSON.stringify(body)); + } catch (e) { + reject(e); + } + + return Response.json({}); + }, + }); + + await $`bun i`.cwd(tmp); + + await spawn({ + cmd: [bunExe(), "pm", "audit"], + cwd: tmp, + env: { + ...bunEnv, + NPM_CONFIG_REGISTRY: server.url.toString(), + }, + }).exited; + + const body = await requestBodyPromise; + + const { stdout, exited } = spawn({ + cmd: [bunExe(), "pm", "audit", "--json"], + cwd: tmp, + stdout: "pipe", + stderr: "ignore", + env: bunEnv, + }); + + await exited; + + const text = await readableStreamToText(stdout); + + result[body] = JSON.parse(text); +} + +await Bun.file(output).write(JSON.stringify(result, null, "\t")); diff --git a/test/cli/install/registry/fixtures/audit/mix-of-safe-and-vulnerable-dependencies/bun.lock b/test/cli/install/registry/fixtures/audit/mix-of-safe-and-vulnerable-dependencies/bun.lock new file mode 100644 index 0000000000..a2c6ff7792 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/mix-of-safe-and-vulnerable-dependencies/bun.lock @@ -0,0 +1,17 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "mix-of-safe-and-vulnerable-dependencies", + "dependencies": { + "is-number": "7.0.0", + "ms": "0.7.0", + }, + }, + }, + "packages": { + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "ms": ["ms@0.7.0", "", {}, "sha512-YmuMMkfOZzzAftlHwiQxFepJx/5rDaYi9o9QanyBCk485BRAyM/vB9XoYlZvglxE/pmAWOiQgrdoE10watiK9w=="], + } +} diff --git a/test/cli/install/registry/fixtures/audit/mix-of-safe-and-vulnerable-dependencies/package.json b/test/cli/install/registry/fixtures/audit/mix-of-safe-and-vulnerable-dependencies/package.json new file mode 100644 index 0000000000..2072d6eac3 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/mix-of-safe-and-vulnerable-dependencies/package.json @@ -0,0 +1,7 @@ +{ + "name": "mix-of-safe-and-vulnerable-dependencies", + "dependencies": { + "is-number": "7.0.0", + "ms": "0.7.0" + } +} diff --git a/test/cli/install/registry/fixtures/audit/safe-is-number@7/bun.lock b/test/cli/install/registry/fixtures/audit/safe-is-number@7/bun.lock new file mode 100644 index 0000000000..aaa896769e --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/safe-is-number@7/bun.lock @@ -0,0 +1,14 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "safe-is-number-7", + "dependencies": { + "is-number": "7.0.0", + }, + }, + }, + "packages": { + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + } +} diff --git a/test/cli/install/registry/fixtures/audit/safe-is-number@7/package.json b/test/cli/install/registry/fixtures/audit/safe-is-number@7/package.json new file mode 100644 index 0000000000..d947c6ecd5 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/safe-is-number@7/package.json @@ -0,0 +1,6 @@ +{ + "name": "safe-is-number-7", + "dependencies": { + "is-number": "7.0.0" + } +} diff --git a/test/cli/install/registry/fixtures/audit/vuln-with-only-dev-dependencies/bun.lock b/test/cli/install/registry/fixtures/audit/vuln-with-only-dev-dependencies/bun.lock new file mode 100644 index 0000000000..eb1ee4ca87 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/vuln-with-only-dev-dependencies/bun.lock @@ -0,0 +1,14 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "vuln-with-only-dev-dependencies", + "devDependencies": { + "ms": "0.7.0", + }, + }, + }, + "packages": { + "ms": ["ms@0.7.0", "", {}, "sha512-YmuMMkfOZzzAftlHwiQxFepJx/5rDaYi9o9QanyBCk485BRAyM/vB9XoYlZvglxE/pmAWOiQgrdoE10watiK9w=="], + } +} diff --git a/test/cli/install/registry/fixtures/audit/vuln-with-only-dev-dependencies/package.json b/test/cli/install/registry/fixtures/audit/vuln-with-only-dev-dependencies/package.json new file mode 100644 index 0000000000..aa143d9592 --- /dev/null +++ b/test/cli/install/registry/fixtures/audit/vuln-with-only-dev-dependencies/package.json @@ -0,0 +1,6 @@ +{ + "name": "vuln-with-only-dev-dependencies", + "devDependencies": { + "ms": "0.7.0" + } +} diff --git a/test/harness.ts b/test/harness.ts index e5acad6f1d..ee3cb2c0cb 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -7,9 +7,9 @@ import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun"; import { heapStats } from "bun:jsc"; -import { fork, ChildProcess } from "child_process"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { readFile, readlink, writeFile, readdir, rm } from "fs/promises"; +import { ChildProcess, fork } from "child_process"; +import { readdir, readFile, readlink, rm, writeFile } from "fs/promises"; import fs, { closeSync, openSync, rmSync } from "node:fs"; import os from "node:os"; import { dirname, isAbsolute, join } from "path"; @@ -46,8 +46,8 @@ export const isFlaky = isCI; export const isBroken = isCI; export const isASAN = basename(process.execPath).includes("bun-asan"); -export const bunEnv: NodeJS.ProcessEnv = { - ...process.env, +export const bunEnv: NodeJS.Dict = { + ...(process.env as NodeJS.Dict), GITHUB_ACTIONS: "false", BUN_DEBUG_QUIET_LOGS: "1", NO_COLOR: "1", @@ -205,7 +205,7 @@ export async function makeTree(base: string, tree: DirectoryTree) { } } -export function makeTreeSync(base: string, tree: DirectoryTree) { +export function makeTreeSyncFromDirectoryTree(base: string, tree: DirectoryTree) { const isDirectoryTree = (value: string | DirectoryTree | Buffer): value is DirectoryTree => typeof value === "object" && value && typeof value?.byteLength === "undefined"; @@ -227,11 +227,20 @@ export function makeTreeSync(base: string, tree: DirectoryTree) { } } +export function makeTreeSync(base: string, filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string) { + if (typeof filesOrAbsolutePathToCopyFolderFrom === "string") { + fs.cpSync(filesOrAbsolutePathToCopyFolderFrom, base, { recursive: true }); + return; + } + + return makeTreeSyncFromDirectoryTree(base, filesOrAbsolutePathToCopyFolderFrom); +} + /** * Recursively create files within a new temporary directory. * * @param basename prefix of the new temporary directory - * @param files directory tree. Each key is a folder or file, and each value is the contents of the file. Use objects for directories. + * @param filesOrAbsolutePathToCopyFolderFrom Directory tree or absolute path to a folder to copy. If passing an object each key is a folder or file, and each value is the contents of the file. Use objects for directories. * @returns an absolute path to the new temporary directory * * @example @@ -244,9 +253,12 @@ export function makeTreeSync(base: string, tree: DirectoryTree) { * }); * ``` */ -export function tempDirWithFiles(basename: string, files: DirectoryTree): string { +export function tempDirWithFiles( + basename: string, + filesOrAbsolutePathToCopyFolderFrom: DirectoryTree | string, +): string { const base = fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()), basename + "_")); - makeTreeSync(base, files); + makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom); return base; } @@ -1652,3 +1664,46 @@ export async function readdirSorted(path: string): Promise { results.sort(); return results; } + +/** + * Helper function for making automatically lazily-executed promises. + * + * The difference is that the promise has not already started to be evaluated when it is created, + * only when you await it does it execute the function. + * + * @example + * ```ts + * function createMyFixture() { + * return { + * start: lazyPromiseLike(() => fetch("https://example.com")), + * stop: lazyPromiseLike(() => fetch("https://example.com")), + * } + * } + * + * const { start, stop } = createMyFixture(); + * + * await start; // Calls only the start function + * ``` + * + * @param fn A function to make lazily evaluated. + * @returns A promise-like object that will evaluate the function when `then` is called. + */ +export function lazyPromiseLike(fn: () => Promise): PromiseLike { + let p: Promise; + + return { + then(onfulfilled, onrejected) { + if (!p) { + p = fn(); + } + return p.then(onfulfilled, onrejected); + }, + }; +} + +export async function gunzipJsonRequest(req: Request) { + const buf = await req.arrayBuffer(); + const inflated = Bun.gunzipSync(buf); + const body = JSON.parse(Buffer.from(inflated).toString("utf-8")); + return body; +}