bun pm audit (#19855)

This commit is contained in:
Alistair Smith
2025-05-23 22:31:12 -07:00
committed by GitHub
parent 76f6574729
commit 9e329ee605
21 changed files with 2302 additions and 8 deletions

View File

@@ -236,6 +236,7 @@ src/ci_info.zig
src/cli.zig src/cli.zig
src/cli/add_command.zig src/cli/add_command.zig
src/cli/add_completions.zig src/cli/add_completions.zig
src/cli/audit_command.zig
src/cli/build_command.zig src/cli/build_command.zig
src/cli/bunx_command.zig src/cli/bunx_command.zig
src/cli/colon_list_type.zig src/cli/colon_list_type.zig

View File

@@ -260,6 +260,7 @@ _bun_pm_completion() {
'hash\:"generate & print the hash of the current lockfile" ' 'hash\:"generate & print the hash of the current lockfile" '
'hash-string\:"print the string used to hash the lockfile" ' 'hash-string\:"print the string used to hash the lockfile" '
'hash-print\:"print the hash stored in the current 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" ' 'cache\:"print the path to the cache folder" '
) )

View File

@@ -95,6 +95,14 @@ To print the hash stored in the current lockfile:
$ bun pm hash-print $ bun pm hash-print
``` ```
## audit
To run a security audit for packages in bun.lock or bun.lockb
```bash
$ bun pm audit
```
## cache ## cache
To print the path to Bun's global module cache: To print the path to Bun's global module cache:

View File

@@ -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 OutdatedCommand = @import("./cli/outdated_command.zig").OutdatedCommand;
pub const PublishCommand = @import("./cli/publish_command.zig").PublishCommand; pub const PublishCommand = @import("./cli/publish_command.zig").PublishCommand;
pub const PackCommand = @import("./cli/pack_command.zig").PackCommand; 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 InitCommand = @import("./cli/init_command.zig").InitCommand;
pub const Arguments = struct { pub const Arguments = struct {

706
src/cli/audit_command.zig Normal file
View File

@@ -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("<r><b>bun pm audit <r><d>v" ++ Global.package_json_version_with_sha ++ "<r>\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("<green>No vulnerabilities found<r>", .{});
printSkippedPackages(packages_result.skipped_packages);
return 0;
}
}
};
fn printSkippedPackages(skipped_packages: std.ArrayList([]const u8)) void {
if (skipped_packages.items.len > 0) {
Output.pretty("<d>Skipped<r> ", .{});
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(" <d>because they do not come from the default registry<r>", .{});
} else {
Output.prettyln(" <d>because it does not come from the default registry<r>", .{});
}
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("<red>error<r>: 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("<green>No vulnerabilities found<r>", .{});
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("<red>{s}<r> {s}", .{ main_vuln.package_name, main_vuln.vulnerable_versions });
} else {
Output.prettyln("<red>{s}<r>", .{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(" <d>{s} <red>{s}<r>", .{ 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(" <d>{s} <red>{s}<r>", .{ vuln_pkg_path, vulnerable_pkg });
}
} else {
Output.prettyln(" <d>(direct dependency)<r>", .{});
}
}
for (package_info.vulnerabilities.items) |vuln| {
if (vuln.title.len > 0) {
if (std.mem.eql(u8, vuln.severity, "critical")) {
Output.prettyln(" <red>critical<d>:<r> {s} - <d>{s}<r>", .{ vuln.title, vuln.url });
} else if (std.mem.eql(u8, vuln.severity, "high")) {
Output.prettyln(" <red>high<d>:<r> {s} - <d>{s}<r>", .{ vuln.title, vuln.url });
} else if (std.mem.eql(u8, vuln.severity, "moderate")) {
Output.prettyln(" <yellow>moderate<d>:<r> {s} - <d>{s}<r>", .{ vuln.title, vuln.url });
} else {
Output.prettyln(" <cyan>low<d>:<r> {s} - <d>{s}<r>", .{ vuln.title, vuln.url });
}
}
}
// if (is_direct_dependency) {
// Output.prettyln(" To fix: <green>`bun update {s}`<r>", .{package_info.name});
// } else {
// Output.prettyln(" To fix: <green>`bun update --latest`<r><d> (may be a breaking change)<r>", .{});
// }
Output.prettyln("", .{});
}
}
const total = vuln_counts.low + vuln_counts.moderate + vuln_counts.high + vuln_counts.critical;
if (total > 0) {
Output.pretty("<b>{d} vulnerabilities<r> (", .{total});
var has_previous = false;
if (vuln_counts.critical > 0) {
Output.pretty("<red><b>{d} critical<r>", .{vuln_counts.critical});
has_previous = true;
}
if (vuln_counts.high > 0) {
if (has_previous) Output.pretty(", ", .{});
Output.pretty("<red>{d} high<r>", .{vuln_counts.high});
has_previous = true;
}
if (vuln_counts.moderate > 0) {
if (has_previous) Output.pretty(", ", .{});
Output.pretty("<yellow>{d} moderate<r>", .{vuln_counts.moderate});
has_previous = true;
}
if (vuln_counts.low > 0) {
if (has_previous) Output.pretty(", ", .{});
Output.pretty("<cyan>{d} low<r>", .{vuln_counts.low});
}
Output.prettyln(")", .{});
Output.prettyln("", .{});
Output.prettyln("To update all dependencies to the latest compatible versions:", .{});
Output.prettyln(" <green>bun update<r>", .{});
Output.prettyln("", .{});
Output.prettyln("To update all dependencies to the latest versions (including breaking changes):", .{});
Output.prettyln(" <green>bun update --latest<r>", .{});
Output.prettyln("", .{});
}
if (total > 0) {
return 1;
}
} else {
Output.writer().writeAll(response_text) catch {};
Output.writer().writeByte('\n') catch {};
}
return 0;
}

View File

@@ -25,6 +25,7 @@ const TrustCommand = @import("./pm_trusted_command.zig").TrustCommand;
const DefaultTrustedCommand = @import("./pm_trusted_command.zig").DefaultTrustedCommand; const DefaultTrustedCommand = @import("./pm_trusted_command.zig").DefaultTrustedCommand;
const Environment = bun.Environment; const Environment = bun.Environment;
pub const PackCommand = @import("./pack_command.zig").PackCommand; pub const PackCommand = @import("./pack_command.zig").PackCommand;
pub const AuditCommand = @import("./audit_command.zig").AuditCommand;
const Npm = Install.Npm; const Npm = Install.Npm;
const PmViewCommand = @import("./pm_view_command.zig"); const PmViewCommand = @import("./pm_view_command.zig");
const File = bun.sys.File; const File = bun.sys.File;
@@ -131,6 +132,7 @@ pub const PackageManagerCommand = struct {
\\ <b><green>bun pm<r> <blue>hash<r> generate & print the hash of the current lockfile \\ <b><green>bun pm<r> <blue>hash<r> generate & print the hash of the current lockfile
\\ <b><green>bun pm<r> <blue>hash-string<r> print the string used to hash the lockfile \\ <b><green>bun pm<r> <blue>hash-string<r> print the string used to hash the lockfile
\\ <b><green>bun pm<r> <blue>hash-print<r> print the hash stored in the current lockfile \\ <b><green>bun pm<r> <blue>hash-print<r> print the hash stored in the current lockfile
\\ <b><green>bun pm<r> <blue>audit<r> check installed packages for vulnerabilities
\\ <b><green>bun pm<r> <blue>cache<r> print the path to the cache folder \\ <b><green>bun pm<r> <blue>cache<r> print the path to the cache folder
\\ <b><green>bun pm<r> <blue>cache rm<r> clear the cache \\ <b><green>bun pm<r> <blue>cache rm<r> clear the cache
\\ <b><green>bun pm<r> <blue>migrate<r> migrate another package manager's lockfile without installing anything \\ <b><green>bun pm<r> <blue>migrate<r> 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); _ = try pm.lockfile.hasMetaHashChanged(true, pm.lockfile.packages.len);
Global.exit(0); 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")) { } else if (strings.eqlComptime(subcommand, "cache")) {
var dir: bun.PathBuffer = undefined; var dir: bun.PathBuffer = undefined;
var fd = pm.getCacheDirectory(); var fd = pm.getCacheDirectory();

View File

@@ -8963,6 +8963,10 @@ pub const PackageManager = struct {
break :brk loader; break :brk loader;
}; };
if (subcommand == .pm and cli.positionals.len >= 2 and strings.eqlComptime(cli.positionals[1], "audit")) {
env.quiet = true;
}
env.loadProcess(); env.loadProcess();
try env.load(entries_option.entries, &[_][]u8{}, .production, false); try env.load(entries_option.entries, &[_][]u8{}, .production, false);

View File

@@ -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
"
`;

View File

@@ -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<string>; stderr: PromiseLike<string>; dir: string }) => Promise<void>;
},
) {
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");
},
});
});

View File

@@ -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"
}
}
]
}
}

View File

@@ -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<string, string[]>, value as AuditReport] as const,
);
export function resolveBulkAdvisoryFixture(request: Record<string, string[]>) {
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<T extends JsonValue>(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;
}

View File

@@ -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=="],
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "express-3",
"dependencies": {
"express": "3"
}
}

View File

@@ -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<string, unknown> = {
"{}": {},
};
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<string>();
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"));

View File

@@ -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=="],
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "mix-of-safe-and-vulnerable-dependencies",
"dependencies": {
"is-number": "7.0.0",
"ms": "0.7.0"
}
}

View File

@@ -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=="],
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "safe-is-number-7",
"dependencies": {
"is-number": "7.0.0"
}
}

View File

@@ -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=="],
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "vuln-with-only-dev-dependencies",
"devDependencies": {
"ms": "0.7.0"
}
}

View File

@@ -7,9 +7,9 @@
import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun"; import { gc as bunGC, sleepSync, spawnSync, unsafe, which, write } from "bun";
import { heapStats } from "bun:jsc"; import { heapStats } from "bun:jsc";
import { fork, ChildProcess } from "child_process";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; 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 fs, { closeSync, openSync, rmSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import { dirname, isAbsolute, join } from "path"; import { dirname, isAbsolute, join } from "path";
@@ -46,8 +46,8 @@ export const isFlaky = isCI;
export const isBroken = isCI; export const isBroken = isCI;
export const isASAN = basename(process.execPath).includes("bun-asan"); export const isASAN = basename(process.execPath).includes("bun-asan");
export const bunEnv: NodeJS.ProcessEnv = { export const bunEnv: NodeJS.Dict<string> = {
...process.env, ...(process.env as NodeJS.Dict<string>),
GITHUB_ACTIONS: "false", GITHUB_ACTIONS: "false",
BUN_DEBUG_QUIET_LOGS: "1", BUN_DEBUG_QUIET_LOGS: "1",
NO_COLOR: "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 => const isDirectoryTree = (value: string | DirectoryTree | Buffer): value is DirectoryTree =>
typeof value === "object" && value && typeof value?.byteLength === "undefined"; 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. * Recursively create files within a new temporary directory.
* *
* @param basename prefix of the 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 * @returns an absolute path to the new temporary directory
* *
* @example * @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 + "_")); const base = fs.mkdtempSync(join(fs.realpathSync(os.tmpdir()), basename + "_"));
makeTreeSync(base, files); makeTreeSync(base, filesOrAbsolutePathToCopyFolderFrom);
return base; return base;
} }
@@ -1652,3 +1664,46 @@ export async function readdirSorted(path: string): Promise<string[]> {
results.sort(); results.sort();
return results; 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<T>(fn: () => Promise<T>): PromiseLike<T> {
let p: Promise<T>;
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;
}