blob: a709a7b5e02d2a4d4f83de52335a7fb3f18e022f [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{anyhow, Context, Result},
ffx_scrutiny_verify_args::component_resolvers::Command,
scrutiny_config::Config,
scrutiny_frontend::{command_builder::CommandBuilder, launcher},
serde::{Deserialize, Serialize},
std::{collections::HashSet, fs, path::PathBuf},
};
type NodePath = String;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct ComponentResolversRequest {
scheme: String,
moniker: NodePath,
protocol: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
struct ComponentResolversResponse {
deps: HashSet<PathBuf>,
monikers: Vec<NodePath>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
struct AllowListEntry {
#[serde(flatten)]
query: ComponentResolversRequest,
components: Vec<NodePath>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
struct AllowList(Vec<AllowListEntry>);
impl AllowList {
pub fn iter(&self) -> impl Iterator<Item = (ComponentResolversRequest, &[NodePath])> {
self.0.iter().map(|entry| (entry.query.clone(), entry.components.as_slice()))
}
}
/// A trait to query scrutiny's verify/component_resolvers API.
trait QueryComponentResolvers {
/// Walk the v2 component tree, finding all components with a component resolver for `scheme`
/// in its environment that has the given `moniker` and has access to `protocol`.
fn query(
&self,
scheme: String,
moniker: NodePath,
protocol: String,
) -> Result<ComponentResolversResponse>;
}
/// An impl of [`QueryComponentResolvers`] that launches and queries scrutiny relative to the
/// current working directory.
#[derive(Debug)]
struct ScrutinyQueryComponentResolvers {
build_path: PathBuf,
update_package_path: PathBuf,
blobfs_paths: Vec<PathBuf>,
}
impl QueryComponentResolvers for ScrutinyQueryComponentResolvers {
fn query(
&self,
scheme: String,
moniker: NodePath,
protocol: String,
) -> Result<ComponentResolversResponse> {
let request = ComponentResolversRequest { scheme, moniker, protocol };
let mut config = Config::run_command_with_plugins(
CommandBuilder::new("verify.component_resolvers")
.param("scheme", &request.scheme)
.param("moniker", request.moniker.to_string())
.param("protocol", &request.protocol)
.build(),
vec!["DevmgrConfigPlugin", "StaticPkgsPlugin", "CorePlugin", "VerifyPlugin"],
);
config.runtime.model.build_path = self.build_path.clone();
config.runtime.model.update_package_path = self.update_package_path.clone();
config.runtime.model.blobfs_paths = self.blobfs_paths.clone();
config.runtime.logging.silent_mode = true;
let results = launcher::launch_from_config(config).context("Failed to launch scrutiny")?;
if results.starts_with("Error: ") {
return Err(anyhow!(results))
.with_context(|| format!("Failed to query scrutiny with {:?}", request));
}
Ok(serde_json5::from_str(&results).context(format!(
"Failed to deserialize verify component resolvers results: {:?}",
results
))?)
}
}
/// For each section of the provided `allowlist`, queries scrutiny for all components configured
/// with a component resolver for `scheme` with the given `moniker` that itself has access
/// to `protocol`. If any components match but are not in the allowlist, returns an allowlist that
/// would allow all found violations. On success, returns the set of files accessed to run the
/// analysis, for depfile generation.
fn verify_component_resolvers(
scrutiny: impl QueryComponentResolvers,
allowlist: AllowList,
) -> Result<Result<HashSet<PathBuf>, AllowList>> {
let mut violations = vec![];
let mut deps = HashSet::new();
for (query, allowed_monikers) in allowlist.iter() {
let allowed_monikers: HashSet<&NodePath> = allowed_monikers.into_iter().collect();
let response = scrutiny
.query(query.scheme.clone(), query.moniker.clone(), query.protocol.clone())
.with_context(|| {
format!("Failed to query verify.capability_component_resolvers with {:?}", query)
})?;
deps.extend(response.deps);
let mut unexpected = vec![];
for moniker in response.monikers {
if !allowed_monikers.contains(&moniker) {
unexpected.push(moniker);
}
}
if !unexpected.is_empty() {
violations.push(AllowListEntry { query, components: unexpected });
}
}
if violations.is_empty() {
Ok(Ok(deps))
} else {
Ok(Err(AllowList(violations)))
}
}
pub async fn verify(cmd: Command) -> Result<HashSet<PathBuf>> {
let allowlist_path = cmd.allowlist.clone();
let scrutiny = ScrutinyQueryComponentResolvers {
build_path: cmd.build_path,
update_package_path: cmd.update,
blobfs_paths: cmd.blobfs,
};
let allowlist: AllowList = serde_json5::from_str(
&fs::read_to_string(&cmd.allowlist).context("Failed to read allowlist")?,
)
.context("Failed to deserialize allowlist")?;
verify_component_resolvers(scrutiny, allowlist)?.map_err(|violations| {
anyhow!(
"
Static Component Resolver Capability Analysis Error:
The component resolver verifier found some components configured to be resolved using
a privileged component resolver.
If it is intended for these components to be resolved using the given resolver, add an entry
to the allowlist located at: {:?}
Verification Errors:
{}",
allowlist_path,
serde_json::to_string_pretty(&violations).unwrap()
)
})
}
#[cfg(test)]
mod tests {
use {super::*, assert_matches::assert_matches, std::collections::HashMap};
#[derive(Debug)]
struct MockQueryComponentResolvers {
responses: HashMap<(String, NodePath, String), String>,
}
impl MockQueryComponentResolvers {
fn new() -> Self {
Self { responses: HashMap::new() }
}
fn with_response(
self,
query: (String, NodePath, String),
response: Vec<NodePath>,
response_deps: Vec<String>,
) -> Self {
let raw_response = serde_json::to_string(&ComponentResolversResponse {
monikers: response,
deps: response_deps.into_iter().map(PathBuf::from).collect(),
})
.unwrap();
self.with_raw_response(query, raw_response)
}
fn with_raw_response(
mut self,
query: (String, NodePath, String),
response: String,
) -> Self {
self.responses.insert(query, response);
self
}
}
impl QueryComponentResolvers for MockQueryComponentResolvers {
fn query(
&self,
scheme: String,
moniker: NodePath,
protocol: String,
) -> Result<ComponentResolversResponse> {
let key = (scheme, moniker, protocol);
let response = self
.responses
.get(&key)
.expect(&format!("mock to be configured for key {:?}", key));
Ok(serde_json5::from_str(&response).context(format!(
"Failed to deserialize verify component resolvers results: {:?}",
response
))?)
}
}
fn parse_allowlist(raw: &str) -> AllowList {
let mut allowlist: AllowList = serde_json5::from_str(raw).unwrap();
for entry in allowlist.0.iter_mut() {
entry.components.sort_unstable();
}
allowlist.0.sort_unstable();
allowlist
}
#[test]
fn fails_on_invalid_response() {
let allowlist = parse_allowlist(
r#"[
{
scheme: "fuchsia-pkg",
moniker: "/core/universe-resolver",
protocol: "fuchsia.pkg.PackageResolver",
components: [
],
},
]"#,
);
let scrutiny = MockQueryComponentResolvers::new().with_raw_response(
(
"fuchsia-pkg".to_owned(),
"/core/universe-resolver".to_owned(),
"fuchsia.pkg.PackageResolver".to_owned(),
),
"invalid".to_owned(),
);
assert_matches!(verify_component_resolvers(scrutiny, allowlist), Err(_));
}
#[test]
fn reports_unexpected_entry() {
let allowlist = parse_allowlist(
r#"[
{
scheme: "fuchsia-pkg",
moniker: "/core/universe-resolver",
protocol: "fuchsia.pkg.PackageResolver",
components: [
"/core/allowed",
],
},
]"#,
);
let violations = parse_allowlist(
r#"[
{
scheme: "fuchsia-pkg",
moniker: "/core/universe-resolver",
protocol: "fuchsia.pkg.PackageResolver",
components: [
"/core/stopme",
],
},
]"#,
);
let scrutiny = MockQueryComponentResolvers::new().with_response(
(
"fuchsia-pkg".to_owned(),
"/core/universe-resolver".to_owned(),
"fuchsia.pkg.PackageResolver".to_owned(),
),
vec!["/core/allowed".to_owned(), "/core/stopme".to_owned()],
vec!["path/to/dep.zbi".to_owned()],
);
assert_eq!(verify_component_resolvers(scrutiny, allowlist).unwrap(), Err(violations));
}
#[test]
fn ignores_unused_allow() {
let allowlist = parse_allowlist(
r#"[
{
scheme: "fuchsia-pkg",
moniker: "/core/universe-resolver",
protocol: "fuchsia.pkg.PackageResolver",
components: [
"/core/allowed",
"/core/also-allowed",
],
},
]"#,
);
let scrutiny = MockQueryComponentResolvers::new().with_response(
(
"fuchsia-pkg".to_owned(),
"/core/universe-resolver".to_owned(),
"fuchsia.pkg.PackageResolver".to_owned(),
),
vec!["/core/allowed".to_owned(), "/core/also-allowed".to_owned()],
vec!["path/to/dep.zbi".to_owned()],
);
let expected_deps = vec!["path/to/dep.zbi".to_string().into()].into_iter().collect();
assert_eq!(verify_component_resolvers(scrutiny, allowlist).unwrap(), Ok(expected_deps));
}
#[test]
fn checks_all_entries() {
let allowlist = parse_allowlist(
r#"[
{
scheme: "a",
moniker: "/core/resolver-a",
protocol: "fuchsia.proto.a",
components: [
"/core/allowed-a",
],
},
{
scheme: "b",
moniker: "/core/resolver-b",
protocol: "fuchsia.proto.b",
components: [
"/core/allowed-b",
],
},
{
scheme: "c",
moniker: "/core/resolver-c",
protocol: "fuchsia.proto.c",
components: [
"/core/allowed-c",
],
},
]"#,
);
let violations = parse_allowlist(
r#"[
{
scheme: "a",
moniker: "/core/resolver-a",
protocol: "fuchsia.proto.a",
components: [
"/core/violation-a",
],
},
{
scheme: "c",
moniker: "/core/resolver-c",
protocol: "fuchsia.proto.c",
components: [
"/core/violation-c",
],
},
]"#,
);
let scrutiny = MockQueryComponentResolvers::new()
.with_response(
("a".to_owned(), "/core/resolver-a".to_owned(), "fuchsia.proto.a".to_owned()),
vec!["/core/allowed-a".to_owned(), "/core/violation-a".to_owned()],
vec!["dep1".to_owned()],
)
.with_response(
("b".to_owned(), "/core/resolver-b".to_owned(), "fuchsia.proto.b".to_owned()),
vec!["/core/allowed-b".to_owned()],
vec!["dep2".to_owned()],
)
.with_response(
("c".to_owned(), "/core/resolver-c".to_owned(), "fuchsia.proto.c".to_owned()),
vec!["/core/allowed-c".to_owned(), "/core/violation-c".to_owned()],
vec!["dep3".to_owned()],
);
assert_eq!(verify_component_resolvers(scrutiny, allowlist).unwrap(), Err(violations));
}
}