use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use fleetreach_core::semver::Version;
use fleetreach_core::{
Ecosystem, Occurrence, RepoId, RepoOutcome, ScanStatus, VulnFinding, WarnFinding,
};
use fleetreach_ghactions::{ghactions_db_path, GhActionsDb, GhaError};
use fleetreach_go::{GoDb, GoError, SandboxPolicy};
use fleetreach_hex::{hex_db_path, HexDb, HexError};
use fleetreach_julia::{julia_db_path, JuliaDb, JuliaError};
use fleetreach_maven::{maven_db_path, MavenDb, MavenError};
use fleetreach_npm::{npm_db_path, NpmDb, NpmError};
use fleetreach_nuget::{nuget_db_path, NuGetDb, NuGetError};
use fleetreach_packagist::{packagist_db_path, PackagistDb, PackagistError};
use fleetreach_pypi::{pypi_db_path, PyPiDb, PyPiError};
use fleetreach_rubygems::{rubygems_db_path, RubyGemsDb, RubyGemsError};
use fleetreach_scan::{scan_lockfile, scan_toolchain, AdvisoryDb, RepoScan};
use fleetreach_swift::{swift_db_path, SwiftDb, SwiftError};
use rayon::prelude::*;
use walkdir::WalkDir;
use crate::config::{Config, Repo};
use crate::resolve;
#[derive(Debug, Default, Clone)]
pub struct ScanData {
pub vulnerabilities: Vec<VulnFinding>,
pub warnings: Vec<WarnFinding>,
pub outcomes: Vec<RepoOutcome>,
pub skipped_unparseable: u32,
}
#[derive(Debug, Clone)]
pub struct Toolchain {
pub channel: String,
pub version: Version,
}
#[derive(Debug, Clone, Copy)]
pub struct GoScan<'a> {
pub govulncheck: Option<&'a Path>,
pub sandbox: SandboxPolicy,
pub vuln_db: Option<&'a str>,
pub offline: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct NpmScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct PyPiScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct RubyGemsScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct PackagistScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct NuGetScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct JuliaScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct SwiftScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct HexScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct GhActionsScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct MavenScan<'a> {
pub vuln_db: Option<&'a str>,
}
#[allow(clippy::too_many_arguments)]
pub fn scan_fleet(
db: &AdvisoryDb,
config: &Config,
toolchain: Option<&Toolchain>,
host_triple: Option<&str>,
go: &GoScan,
npm: &NpmScan,
pypi: &PyPiScan,
rubygems: &RubyGemsScan,
packagist: &PackagistScan,
nuget: &NuGetScan,
julia: &JuliaScan,
swift: &SwiftScan,
hex: &HexScan,
ghactions: &GhActionsScan,
maven: &MavenScan,
) -> ScanData {
let mut data = ScanData::default();
let npm_db: Option<Result<NpmDb, NpmError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Npm)
{
npm.vuln_db
.and_then(npm_db_path)
.map(|root| NpmDb::load(&root))
} else {
None
};
let go_db: Option<Result<GoDb, GoError>> = if go.govulncheck.is_none()
&& config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Go)
{
go.vuln_db
.and_then(fleetreach_go::offline_db_path)
.map(|root| GoDb::load(&root))
} else {
None
};
let pypi_db: Option<Result<PyPiDb, PyPiError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Pypi)
{
pypi.vuln_db
.and_then(pypi_db_path)
.map(|root| PyPiDb::load(&root))
} else {
None
};
let rubygems_db: Option<Result<RubyGemsDb, RubyGemsError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::RubyGems)
{
rubygems
.vuln_db
.and_then(rubygems_db_path)
.map(|root| RubyGemsDb::load(&root))
} else {
None
};
let packagist_db: Option<Result<PackagistDb, PackagistError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Packagist)
{
packagist
.vuln_db
.and_then(packagist_db_path)
.map(|root| PackagistDb::load(&root))
} else {
None
};
let nuget_db: Option<Result<NuGetDb, NuGetError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::NuGet)
{
nuget
.vuln_db
.and_then(nuget_db_path)
.map(|root| NuGetDb::load(&root))
} else {
None
};
let julia_db: Option<Result<JuliaDb, JuliaError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Julia)
{
julia
.vuln_db
.and_then(julia_db_path)
.map(|root| JuliaDb::load(&root))
} else {
None
};
let swift_db: Option<Result<SwiftDb, SwiftError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Swift)
{
swift
.vuln_db
.and_then(swift_db_path)
.map(|root| SwiftDb::load(&root))
} else {
None
};
let hex_db: Option<Result<HexDb, HexError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Hex)
{
hex.vuln_db
.and_then(hex_db_path)
.map(|root| HexDb::load(&root))
} else {
None
};
let ghactions_db: Option<Result<GhActionsDb, GhaError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::GitHubActions)
{
ghactions
.vuln_db
.and_then(ghactions_db_path)
.map(|root| GhActionsDb::load(&root))
} else {
None
};
let maven_db: Option<Result<MavenDb, MavenError>> = if config
.repos
.iter()
.any(|r| effective_ecosystem(r) == Ecosystem::Maven)
{
maven
.vuln_db
.and_then(maven_db_path)
.map(|root| MavenDb::load(&root))
} else {
None
};
let per_repo: Vec<RepoResult> = config
.repos
.par_iter()
.map(|repo| {
scan_one_repo(
db,
repo,
host_triple,
go,
npm_db.as_ref(),
go_db.as_ref(),
pypi_db.as_ref(),
rubygems_db.as_ref(),
packagist_db.as_ref(),
nuget_db.as_ref(),
julia_db.as_ref(),
swift_db.as_ref(),
hex_db.as_ref(),
ghactions_db.as_ref(),
maven_db.as_ref(),
)
})
.collect();
for result in per_repo {
data.vulnerabilities.extend(result.vulnerabilities);
data.warnings.extend(result.warnings);
data.outcomes.push(result.outcome);
data.skipped_unparseable += result.skipped_unparseable;
}
if let Some(tc) = toolchain {
let ts = scan_toolchain(db, &tc.channel, &tc.version);
data.vulnerabilities.extend(ts.vulnerabilities);
data.warnings.extend(ts.warnings);
}
data
}
struct RepoResult {
vulnerabilities: Vec<VulnFinding>,
warnings: Vec<WarnFinding>,
outcome: RepoOutcome,
skipped_unparseable: u32,
}
impl RepoResult {
fn scanned(
repo: &RepoId,
vulnerabilities: Vec<VulnFinding>,
warnings: Vec<WarnFinding>,
) -> Self {
let outcome = RepoOutcome {
repo: repo.clone(),
status: ScanStatus::Scanned {
vulns: vulnerabilities.len(),
warnings: warnings.len(),
},
};
RepoResult {
vulnerabilities,
warnings,
outcome,
skipped_unparseable: 0,
}
}
fn with_skipped(mut self, skipped_unparseable: u32) -> Self {
self.skipped_unparseable = skipped_unparseable;
self
}
fn errored(repo: &RepoId, reason: String) -> Self {
RepoResult {
vulnerabilities: Vec::new(),
warnings: Vec::new(),
outcome: RepoOutcome {
repo: repo.clone(),
status: ScanStatus::Errored { reason },
},
skipped_unparseable: 0,
}
}
}
fn effective_ecosystem(repo: &Repo) -> Ecosystem {
if let Some(eco) = repo.ecosystem {
return eco;
}
if repo.path.join("Cargo.lock").is_file() {
return Ecosystem::Cargo;
}
if repo.path.join("go.mod").is_file() {
return Ecosystem::Go;
}
if repo.path.join("package-lock.json").is_file() {
return Ecosystem::Npm;
}
if fleetreach_pypi::detect(&repo.path).is_some() {
return Ecosystem::Pypi;
}
if repo.path.join("Gemfile.lock").is_file() {
return Ecosystem::RubyGems;
}
if repo.path.join("composer.lock").is_file() {
return Ecosystem::Packagist;
}
if repo.path.join("packages.lock.json").is_file() {
return Ecosystem::NuGet;
}
if repo.path.join("Manifest.toml").is_file() {
return Ecosystem::Julia;
}
if repo.path.join("Package.resolved").is_file() {
return Ecosystem::Swift;
}
if repo.path.join("mix.lock").is_file() {
return Ecosystem::Hex;
}
if repo.path.join("gradle.lockfile").is_file() || repo.path.join("pom.xml").is_file() {
return Ecosystem::Maven;
}
if repo.path.join(".github").join("workflows").is_dir() {
return Ecosystem::GitHubActions;
}
Ecosystem::Cargo
}
#[allow(clippy::too_many_arguments)]
fn scan_one_repo(
db: &AdvisoryDb,
repo: &Repo,
host_triple: Option<&str>,
go: &GoScan,
npm_db: Option<&Result<NpmDb, NpmError>>,
go_db: Option<&Result<GoDb, GoError>>,
pypi_db: Option<&Result<PyPiDb, PyPiError>>,
rubygems_db: Option<&Result<RubyGemsDb, RubyGemsError>>,
packagist_db: Option<&Result<PackagistDb, PackagistError>>,
nuget_db: Option<&Result<NuGetDb, NuGetError>>,
julia_db: Option<&Result<JuliaDb, JuliaError>>,
swift_db: Option<&Result<SwiftDb, SwiftError>>,
hex_db: Option<&Result<HexDb, HexError>>,
ghactions_db: Option<&Result<GhActionsDb, GhaError>>,
maven_db: Option<&Result<MavenDb, MavenError>>,
) -> RepoResult {
match effective_ecosystem(repo) {
Ecosystem::Go => return scan_go_repo(repo, go, go_db),
Ecosystem::Npm => return scan_npm_repo(repo, npm_db),
Ecosystem::Pypi => return scan_pypi_repo(repo, pypi_db),
Ecosystem::RubyGems => return scan_rubygems_repo(repo, rubygems_db),
Ecosystem::Packagist => return scan_packagist_repo(repo, packagist_db),
Ecosystem::NuGet => return scan_nuget_repo(repo, nuget_db),
Ecosystem::Julia => return scan_julia_repo(repo, julia_db),
Ecosystem::Swift => return scan_swift_repo(repo, swift_db),
Ecosystem::Hex => return scan_hex_repo(repo, hex_db),
Ecosystem::Maven => return scan_maven_repo(repo, maven_db),
Ecosystem::GitHubActions => return scan_ghactions_repo(repo, ghactions_db),
Ecosystem::Cargo => {}
}
let (lockfiles, walk_errors) = discover_lockfiles(repo);
let mut vulnerabilities: Vec<VulnFinding> = Vec::new();
let mut warnings: Vec<WarnFinding> = Vec::new();
let mut error: Option<String> = None;
if !walk_errors.is_empty() {
error.get_or_insert_with(|| {
format!(
"could not fully walk {}: {}",
repo.path.display(),
walk_errors.join("; ")
)
});
}
if lockfiles.is_empty() {
error.get_or_insert_with(|| format!("no Cargo.lock found under {}", repo.path.display()));
}
for lockfile in &lockfiles {
match scan_lockfile(db, &repo.id, lockfile) {
Ok(mut scan) => {
if let (Some(host), Some(dir)) = (host_triple, lockfile.parent()) {
if let Ok(built) = resolve::built_package_set(dir, host) {
annotate_built(&mut scan, &built);
}
}
vulnerabilities.extend(scan.vulnerabilities);
warnings.extend(scan.warnings);
}
Err(e) => {
error.get_or_insert_with(|| e.to_string());
}
}
}
match error {
Some(reason) => RepoResult {
outcome: RepoOutcome {
repo: repo.id.clone(),
status: ScanStatus::Errored { reason },
},
vulnerabilities,
warnings,
skipped_unparseable: 0,
},
None => RepoResult::scanned(&repo.id, vulnerabilities, warnings),
}
}
fn scan_go_repo(repo: &Repo, go: &GoScan, go_db: Option<&Result<GoDb, GoError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let scanned = |vulnerabilities| RepoResult::scanned(&repo.id, vulnerabilities, Vec::new());
let Some(govulncheck) = go.govulncheck else {
return match go_db {
Some(Ok(db)) => match fleetreach_go::scan_offline(&repo.path, db, &repo.id) {
Ok(vulnerabilities) => scanned(vulnerabilities),
Err(e) => errored(format!("tier-c offline scan: {e}")),
},
Some(Err(e)) => errored(format!("tier-c offline DB: {e}")),
None => errored(
"Go repo (go.mod): no govulncheck available (needs --allow-untrusted-builds \
and a govulncheck binary via --govulncheck <path> or PATH), and no offline \
DB mirror for the toolchain-free Tier-C fallback (pass \
--go-vuln-db=file://<mirror>)"
.to_string(),
),
};
};
let opts = fleetreach_go::GoScanOptions {
govulncheck,
sandbox: go.sandbox,
vuln_db: go.vuln_db,
offline: go.offline,
};
match fleetreach_go::scan_module(&repo.path, &repo.id, &opts) {
Ok(vulnerabilities) => scanned(vulnerabilities),
Err(e) => errored(format!("govulncheck: {e}")),
}
}
fn scan_npm_repo(repo: &Repo, npm_db: Option<&Result<NpmDb, NpmError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match npm_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("npm OSV DB: {e}")),
None => {
return errored(
"npm repo (package-lock.json): no OSV DB mirror for the toolchain-free \
matcher (pass --npm-vuln-db=file://<dir>, e.g. an unzipped osv.dev npm export)"
.to_string(),
)
}
};
match fleetreach_npm::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("npm tier-c scan: {e}")),
}
}
fn scan_pypi_repo(repo: &Repo, pypi_db: Option<&Result<PyPiDb, PyPiError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match pypi_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("PyPI OSV DB: {e}")),
None => {
return errored(
"PyPI repo (uv.lock/poetry.lock/Pipfile.lock): no OSV DB mirror for the \
toolchain-free matcher (pass --pypi-vuln-db=file://<path>, e.g. the osv.dev \
PyPI export all.zip or an unzipped directory)"
.to_string(),
)
}
};
match fleetreach_pypi::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("pypi tier-c scan: {e}")),
}
}
fn scan_rubygems_repo(
repo: &Repo,
rubygems_db: Option<&Result<RubyGemsDb, RubyGemsError>>,
) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match rubygems_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("RubyGems OSV DB: {e}")),
None => {
return errored(
"RubyGems repo (Gemfile.lock): no OSV DB mirror for the toolchain-free \
matcher (pass --rubygems-vuln-db=file://<path>, e.g. the osv.dev RubyGems \
export all.zip or an unzipped directory)"
.to_string(),
)
}
};
match fleetreach_rubygems::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("rubygems tier-c scan: {e}")),
}
}
fn scan_packagist_repo(
repo: &Repo,
packagist_db: Option<&Result<PackagistDb, PackagistError>>,
) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match packagist_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("Packagist OSV DB: {e}")),
None => {
return errored(
"Packagist repo (composer.lock): no OSV DB mirror for the toolchain-free \
matcher (pass --packagist-vuln-db=file://<path>, e.g. the osv.dev Packagist \
export all.zip or an unzipped directory)"
.to_string(),
)
}
};
match fleetreach_packagist::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("packagist tier-c scan: {e}")),
}
}
fn scan_nuget_repo(repo: &Repo, nuget_db: Option<&Result<NuGetDb, NuGetError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match nuget_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("NuGet OSV DB: {e}")),
None => {
return errored(
"NuGet repo (packages.lock.json): no OSV DB mirror for the toolchain-free \
matcher (pass --nuget-vuln-db=file://<path>, e.g. the osv.dev NuGet export \
all.zip or an unzipped directory)"
.to_string(),
)
}
};
match fleetreach_nuget::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("nuget tier-c scan: {e}")),
}
}
fn scan_julia_repo(repo: &Repo, julia_db: Option<&Result<JuliaDb, JuliaError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match julia_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("Julia OSV DB: {e}")),
None => {
return errored(
"Julia repo (Manifest.toml): no OSV DB mirror for the toolchain-free matcher \
(pass --julia-vuln-db=file://<path>, e.g. the osv.dev Julia export all.zip or \
an unzipped directory)"
.to_string(),
)
}
};
match fleetreach_julia::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("julia tier-c scan: {e}")),
}
}
fn scan_swift_repo(repo: &Repo, swift_db: Option<&Result<SwiftDb, SwiftError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db =
match swift_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("Swift OSV DB: {e}")),
None => return errored(
"Swift repo (Package.resolved): no OSV DB mirror for the toolchain-free matcher \
(pass --swift-vuln-db=file://<path>, e.g. the osv.dev SwiftURL export all.zip \
or an unzipped directory)"
.to_string(),
),
};
match fleetreach_swift::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("swift tier-c scan: {e}")),
}
}
fn scan_hex_repo(repo: &Repo, hex_db: Option<&Result<HexDb, HexError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match hex_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("Hex OSV DB: {e}")),
None => {
return errored(
"Hex repo (mix.lock): no OSV DB mirror for the toolchain-free matcher (pass \
--hex-vuln-db=file://<path>, e.g. the osv.dev Hex export all.zip or an \
unzipped directory)"
.to_string(),
)
}
};
match fleetreach_hex::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("hex tier-c scan: {e}")),
}
}
fn scan_maven_repo(repo: &Repo, maven_db: Option<&Result<MavenDb, MavenError>>) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db =
match maven_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("Maven OSV DB: {e}")),
None => return errored(
"Maven repo (gradle.lockfile/pom.xml): no OSV DB mirror for the toolchain-free \
matcher (pass --maven-vuln-db=file://<path>, e.g. the osv.dev Maven export \
all.zip or an unzipped directory)"
.to_string(),
),
};
match fleetreach_maven::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("maven tier-c scan: {e}")),
}
}
fn scan_ghactions_repo(
repo: &Repo,
ghactions_db: Option<&Result<GhActionsDb, GhaError>>,
) -> RepoResult {
let errored = |reason: String| RepoResult::errored(&repo.id, reason);
let db = match ghactions_db {
Some(Ok(db)) => db,
Some(Err(e)) => return errored(format!("GitHub Actions OSV DB: {e}")),
None => {
return errored(
"GitHub Actions repo (.github/workflows): no OSV DB mirror for the \
toolchain-free matcher (pass --ghactions-vuln-db=file://<path>, e.g. the \
osv.dev GitHub Actions export all.zip or an unzipped directory)"
.to_string(),
)
}
};
match fleetreach_ghactions::scan_offline(&repo.path, db, &repo.id) {
Ok(scan) => RepoResult::scanned(&repo.id, scan.findings, Vec::new())
.with_skipped(scan.skipped_unparseable),
Err(e) => errored(format!("github-actions tier-c scan: {e}")),
}
}
fn annotate_built(scan: &mut RepoScan, built: &BTreeSet<(String, Version)>) {
let occurrences = scan
.vulnerabilities
.iter_mut()
.flat_map(|v| v.occurrences.iter_mut())
.chain(
scan.warnings
.iter_mut()
.flat_map(|w| w.occurrences.iter_mut()),
);
for occurrence in occurrences {
if let Occurrence::InRepo {
package,
installed,
active,
..
} = occurrence
{
*active = Some(built.contains(&(package.clone(), installed.clone())));
}
}
}
pub fn discover_lockfiles(repo: &Repo) -> (Vec<PathBuf>, Vec<String>) {
if !repo.glob {
let lock = repo.path.join("Cargo.lock");
return (if lock.is_file() { vec![lock] } else { vec![] }, Vec::new());
}
let mut paths = Vec::new();
let mut errors = Vec::new();
for entry in WalkDir::new(&repo.path).max_depth(repo.glob_max_depth) {
match entry {
Ok(e) if e.file_type().is_file() && e.file_name() == "Cargo.lock" => {
paths.push(e.into_path());
}
Ok(_) => {}
Err(e) => errors.push(e.to_string()),
}
}
(paths, errors)
}