pub mod build;
pub mod supervise;
use amiss_git::{GitResources, ObjectKind, Repository};
use amiss_wire::action::{executable_platform, host_platform};
use amiss_wire::controls::{ConstraintPlatform, ExecutionConstraintDescriptor, GitMode};
use amiss_wire::digest::{Digest, RAW_EVIDENCE_DOMAIN, hb, sha256};
use amiss_wire::manifest::{ReleaseArtifact, ReleaseManifest, RuntimeRole};
use amiss_wire::model::{Oid, RepoPathText};
pub use amiss_wire::report::ENGINE_DOMAIN;
pub const BOOTSTRAP_DOMAIN: &str = "amiss/scanner-action-bootstrap";
pub const ACTION_METADATA_PATH: &str = "action.yml";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Refusal {
UnsupportedPlatform,
ActionTree,
ActionMetadata,
PathNotRegularBlob,
ManifestUnreadable,
ManifestDigest,
DependencyLock,
ArtifactSelection,
RuntimeClosure,
EngineDigest,
PlatformBinding,
BootstrapDigest,
}
#[derive(Clone, Debug)]
pub struct Validated {
pub manifest: ReleaseManifest,
pub artifact: ReleaseArtifact,
pub platform: ConstraintPlatform,
pub engine_digest: Digest,
pub binary: Vec<u8>,
}
pub fn validate(
action: &Repository,
resources: &mut GitResources,
constraint: &ExecutionConstraintDescriptor,
bootstrap_bytes: &[u8],
) -> Result<Validated, Refusal> {
if hb(BOOTSTRAP_DOMAIN, bootstrap_bytes) != constraint.bootstrap_digest {
return Err(Refusal::BootstrapDigest);
}
let platform = host_platform().ok_or(Refusal::UnsupportedPlatform)?;
if platform != constraint.selected_platform {
return Err(Refusal::PlatformBinding);
}
let tree = action_tree(action, resources, constraint)?;
let (manifest_bytes, manifest_mode) =
blob(action, resources, &tree, &constraint.manifest_path)?;
if manifest_mode != GitMode::RegularFile {
return Err(Refusal::PathNotRegularBlob);
}
let manifest =
ReleaseManifest::parse(&manifest_bytes).map_err(|_defect| Refusal::ManifestUnreadable)?;
if manifest.digest != constraint.release_manifest_digest {
return Err(Refusal::ManifestDigest);
}
for (lock_path, lock_digest) in &manifest.dependency_lock.files {
let (lock_bytes, lock_mode) = blob(action, resources, &tree, lock_path)?;
if lock_mode != GitMode::RegularFile {
return Err(Refusal::PathNotRegularBlob);
}
if hb(RAW_EVIDENCE_DOMAIN, &lock_bytes) != *lock_digest {
return Err(Refusal::DependencyLock);
}
}
let artifact = manifest
.artifacts
.iter()
.find(|candidate| candidate.platform == platform)
.cloned()
.ok_or(Refusal::ArtifactSelection)?;
let mut binary: Option<Vec<u8>> = None;
for file in &artifact.runtime_files {
let (bytes, mode) = blob(action, resources, &tree, &file.path)?;
if mode != file.git_mode {
return Err(Refusal::RuntimeClosure);
}
if sha256(&bytes) != file.file_sha256 {
return Err(Refusal::RuntimeClosure);
}
if file.role == RuntimeRole::Executable {
binary = Some(bytes);
}
}
let binary = binary.ok_or(Refusal::RuntimeClosure)?;
if sha256(&binary) != artifact.binary_sha256 {
return Err(Refusal::EngineDigest);
}
let engine_digest = hb(ENGINE_DOMAIN, &binary);
if engine_digest != artifact.engine_digest {
return Err(Refusal::EngineDigest);
}
if executable_platform(&binary) != Some(platform) {
return Err(Refusal::PlatformBinding);
}
let _launcher = artifact.launcher().ok_or(Refusal::RuntimeClosure)?;
let action_pinned = artifact.runtime_files.iter().any(|file| {
file.role == RuntimeRole::RuntimeData && file.path.as_str() == ACTION_METADATA_PATH
});
if !action_pinned {
return Err(Refusal::ActionMetadata);
}
Ok(Validated {
manifest,
artifact,
platform,
engine_digest,
binary,
})
}
fn action_tree(
action: &Repository,
resources: &mut GitResources,
constraint: &ExecutionConstraintDescriptor,
) -> Result<Oid, Refusal> {
if action.object_format() != constraint.action_object_format {
return Err(Refusal::ActionTree);
}
let object = action
.read_expected(resources, &constraint.action_commit_oid, ObjectKind::Commit)
.map_err(|_defect| Refusal::ActionTree)?;
let commit = amiss_git::parse_commit(action.object_format(), &object.body)
.map_err(|_defect| Refusal::ActionTree)?;
if commit.tree != constraint.action_tree_oid {
return Err(Refusal::ActionTree);
}
Ok(commit.tree)
}
fn blob(
action: &Repository,
resources: &mut GitResources,
tree: &Oid,
path: &RepoPathText,
) -> Result<(Vec<u8>, GitMode), Refusal> {
let mut current = tree.clone();
let mut segments = path.as_str().split('/').peekable();
while let Some(segment) = segments.next() {
let object = action
.read_expected(resources, ¤t, ObjectKind::Tree)
.map_err(|_defect| Refusal::PathNotRegularBlob)?;
let entries = amiss_git::parse_tree(action.object_format(), &object.body)
.map_err(|_defect| Refusal::PathNotRegularBlob)?;
let entry = entries
.iter()
.find(|entry| entry.name == segment.as_bytes())
.ok_or(Refusal::PathNotRegularBlob)?;
if segments.peek().is_some() {
if entry.mode != GitMode::Tree {
return Err(Refusal::PathNotRegularBlob);
}
current = entry.oid.clone();
continue;
}
let mode = entry.mode;
if mode != GitMode::RegularFile && mode != GitMode::ExecutableFile {
return Err(Refusal::PathNotRegularBlob);
}
let object = action
.read_expected(resources, &entry.oid, ObjectKind::Blob)
.map_err(|_defect| Refusal::PathNotRegularBlob)?;
return Ok((object.body, mode));
}
Err(Refusal::PathNotRegularBlob)
}