Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2706,6 +2706,21 @@ An export with nothing left that an index resolves is not handed to guarddog
at all: guarddog handed an empty list answers `[]`, which this section reads as
a network failure.

`guarddog npm verify` reads a `package.json`'s `dependencies` and asks npm for
each by name, so an npm git dependency is the same 404. Its git dependencies
(`git+<url>#<ref>`, `git://...`, `github:`, `gitlab:` and `bitbucket:`
specifiers, and the `owner/repo` shorthand) are taken out and guarddog is
handed the manifest without them; a manifest with none is handed over as it
is. Each git dependency is held to the same first-party rule and the same
remote check. Its commit is the one `bun.lock` or `package-lock.json` records,
read from the manifest's directory and each one above it up to the
repository root. Its `#<ref>`, where it names one, must point at that commit
if it is a tag; if it is a branch, the commit must be what some ref points at,
as for a commit the lock names alone. A git dependency no lock records a
commit for, and whose `#` is not itself a commit, is refused by name: there is
no pin to check. A manifest left with no dependencies is not handed to
guarddog, for the same `[]`.

### Waiving a confirmed guarddog false positive

Because the findings are read here, a finding somebody has looked at and judged
Expand Down
77 changes: 65 additions & 12 deletions src/supply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,32 @@ struct Ledger {
read: BTreeSet<&'static str>,
}

/// Who is first party here, as the policy declares it.
fn first_party_of<'a>(policy: &'a Policy, root: &Path) -> references::FirstParty<'a> {
references::FirstParty {
owner: policy
.declared_owner(root)
.map_err(|error| error.to_string()),
host: policy
.supply_chain
.forge_host
.as_deref()
.unwrap_or(references::DEFAULT_FORGE_HOST),
}
}

/// The npm locks that may say what a manifest in `directory` resolved, nearest
/// first: its own, then each directory up to `root`, where a workspace keeps
/// the one lock for every member.
fn npm_locks(directory: &Path, root: &Path) -> Vec<String> {
directory
.ancestors()
.take_while(|ancestor| ancestor.starts_with(root))
.flat_map(|ancestor| ["bun.lock", "package-lock.json"].map(|name| ancestor.join(name)))
.filter_map(|lock| std::fs::read_to_string(lock).ok())
.collect()
}

fn guarddog(root: &Path, scope: &Scope, policy: &Policy) -> Result<Section> {
let waivers = policy.supply_chain.waive.as_slice();
let (python, npm) = match scope {
Expand Down Expand Up @@ -1535,16 +1561,7 @@ fn guarddog(root: &Path, scope: &Scope, policy: &Policy) -> Result<Section> {
refused = true;
}
for pin in &sorted.git {
let deciding = first_party.get_or_insert_with(|| references::FirstParty {
owner: policy
.declared_owner(root)
.map_err(|error| error.to_string()),
host: policy
.supply_chain
.forge_host
.as_deref()
.unwrap_or(references::DEFAULT_FORGE_HOST),
});
let deciding = first_party.get_or_insert_with(|| first_party_of(policy, root));
match references::check(pin, deciding, directory, &mut remotes) {
references::Checked::Holds(said) => println!(" first party: {said}"),
references::Checked::Refused(said) => {
Expand Down Expand Up @@ -1593,14 +1610,50 @@ fn guarddog(root: &Path, scope: &Scope, policy: &Policy) -> Result<Section> {
for manifest in npm {
let directory = manifest.parent().unwrap_or(root);
checked += 1;
let at = directory.display().to_string();
let sorted = std::fs::read_to_string(&manifest)
.map(|text| references::sort_npm(&text, &npm_locks(directory, root)))
.unwrap_or_default();
for said in &sorted.refused {
println!(" FAILED: guarddog npm: {at}: {said}");
refused = true;
}
for pin in &sorted.git {
let deciding = first_party.get_or_insert_with(|| first_party_of(policy, root));
match references::check(pin, deciding, directory, &mut remotes) {
references::Checked::Holds(said) => println!(" first party: {said}"),
references::Checked::Refused(said) => {
println!(" FAILED: guarddog npm: {at}: {said}");
refused = true;
}
references::Checked::Unread(said) => unrun = unrun.or(Some(said)),
}
}
// The manifest itself where nothing came out of it; otherwise what is
// left, unless nothing is, which guarddog would answer with `[]`.
let rewritten = match &sorted.kept {
None => None,
Some(_) if sorted.indexed == 0 => {
println!(
" {at}: no dependency here resolves from npm, so guarddog was not asked"
);
continue;
}
Some(kept) => Some(tempfile_guard::TempFile::containing(kept)?),
};
let status = Command::new("guarddog")
.args(["npm", "verify", "--output-format", "json", "package.json"])
.args(["npm", "verify", "--output-format", "json"])
.arg(
rewritten
.as_ref()
.map_or_else(|| Path::new("package.json"), |file| file.path.as_path()),
)
.args(GUARDDOG_RULES)
.current_dir(directory)
.output()
.map_err(|error| Fatal::new(format!("could not run guarddog: {error}")))?;
match guarddog_read(
&directory.display().to_string(),
&at,
"npm",
waivers,
&mut ledger,
Expand Down
Loading
Loading