#![allow(unused_parens)]
use crate::clock;
use crate::collection_names::open_configured;
use crate::decide::{self, Resolution};
#[cfg(test)]
use crate::posture_finding::finding_id;
use crate::posture_finding::{
commit_message_location, finding_entity, git_probe, Carrier, GitObjects, Inner, Location,
};
use crate::schemas::decide::DEFAULT_SCOPE_ID as DEFAULT_DECIDE_SCOPE_ID;
#[cfg(any(feature = "local-embed", test))]
use crate::schemas::embeddings::{self, Embedding768};
use crate::schemas::posture::{
modality, posture, DEFAULT_POLICY_SCOPE_ID, DEFAULT_SCAN_SCOPE_ID, DOC_UNSUPPORTED,
EXEMPLAR_PROTECTED, KIND_CHANNEL, KIND_DOCUMENT, KIND_FINDING, KIND_LEGACY_BRIDGE,
KIND_OMISSION, KIND_POLICY_REVISION, KIND_SCAN, KIND_SIGHTING, KIND_TERM, OUTCOME_EXAMINED,
OUTCOME_PARSE_FAILED,
};
#[cfg(test)]
use crate::schemas::posture::{CARRIER_CONTAINER_MEMBER, CARRIER_GIT_BLOB, CARRIER_GIT_COMMIT};
#[cfg(any(feature = "local-embed", test))]
use crate::schemas::posture::{EXEMPLAR_BENIGN, KIND_EXEMPLAR};
#[cfg(test)]
use crate::storage::open_pile_strict;
use crate::storage::{AcquiringReader, FactArchive, FacultySnapshot};
#[cfg(test)]
use crate::storage::FactView;
use anyhow::{anyhow, bail, Context, Result};
use hifitime::Epoch;
use lopdf::{Dictionary, Document, Object};
use regex::Regex;
use std::collections::{BTreeMap, BTreeSet};
use std::io::Read;
use std::path::{Path, PathBuf};
use triblespace::core::blob::encodings::simplearchive::SimpleArchive;
use triblespace::core::blob::encodings::succinctarchive::{
Rank9AcceleratedSuccinctArchiveBlob, SuccinctArchiveBlob,
};
use triblespace::core::collection::{Collection, CollectionCommit, CollectionStoreExt};
use triblespace::core::metadata;
type PileSnapshot = AcquiringReader<FacultySnapshot>;
use triblespace::core::repo::BlobStoreGet;
use triblespace::prelude::*;
type TextHandle = Inline<inlineencodings::Handle<blobencodings::UTF8String>>;
type IntervalValue = Inline<inlineencodings::NsTAIInterval>;
#[derive(Clone, Copy, Debug)]
pub struct DocumentInput<'a> {
pub name: &'a str,
pub bytes: &'a [u8],
}
#[derive(Clone, Copy, Debug)]
pub struct TextInput<'a> {
pub name: &'a str,
pub text: &'a str,
}
#[derive(Debug)]
pub struct ScanReport {
pub scan_id: Option<Id>,
pub target: String,
pub files: Vec<ScannedFile>,
pub omissions: Vec<WalkOmission>,
pub checked: BTreeSet<Id>,
pub unchecked: BTreeSet<Id>,
}
#[derive(Debug)]
pub struct FindingExample {
pub id: Id,
pub evidence: String,
pub value: String,
}
#[derive(Debug)]
pub struct FindingGroup {
pub name: &'static str,
pub count: usize,
pub examples: Vec<FindingExample>,
}
#[derive(Debug)]
pub struct FindingList {
pub scan: Option<Id>,
pub hidden: usize,
pub include_resolved: bool,
pub groups: Vec<FindingGroup>,
}
#[derive(Clone, Copy, Debug)]
pub struct ListOptions {
pub scan: Option<Id>,
pub examples: usize,
pub include_resolved: bool,
}
impl Default for ListOptions {
fn default() -> Self {
Self {
scan: None,
examples: 3,
include_resolved: false,
}
}
}
#[derive(Debug)]
pub struct Coverage {
pub scan: Id,
pub target: String,
pub checked: BTreeSet<Id>,
pub unchecked: BTreeSet<Id>,
pub outcomes: BTreeMap<Id, usize>,
pub omissions: Vec<WalkOmission>,
}
#[derive(Debug)]
pub struct ScanRecord {
pub id: Id,
pub created_at: i128,
pub findings: usize,
pub target: String,
}
#[derive(Debug)]
pub enum VocabularyState {
Missing,
Forked(Vec<Id>),
Ready {
revision: Id,
terms: Vec<(String, String)>,
},
}
#[derive(Debug)]
pub struct VocabularyChannel {
pub id: Id,
pub name: String,
pub state: VocabularyState,
}
#[derive(Clone, Copy, Debug)]
pub enum PolicyMemberKind {
Term,
Exemplar { benign: bool, words: usize },
}
#[derive(Debug)]
pub struct PolicyReceipt {
pub channel_id: Id,
pub channel: String,
pub member: Id,
pub revision: Option<Id>,
pub published: bool,
pub kind: PolicyMemberKind,
pub text: String,
}
#[derive(Debug)]
pub struct GitReport {
pub scan_id: Id,
pub channel: String,
pub protected_terms: usize,
pub lexical_checked: bool,
pub range: String,
pub commits: usize,
pub added_lines: usize,
pub removed_lines: usize,
pub hidden: usize,
pub hits: BTreeMap<String, Vec<GitHit>>,
pub unsafe_attribute_hits: Vec<GitHit>,
pub protected_finding_ids: BTreeMap<Location, Id>,
pub unsafe_finding_ids: BTreeMap<Location, Id>,
}
impl GitReport {
pub fn blocked(&self) -> bool {
git_audit_must_fail(
self.lexical_checked,
self.hits.values().map(Vec::len).sum(),
self.unsafe_attribute_hits.len(),
)
}
}
#[derive(Debug)]
pub struct HookReport {
pub installed: Vec<PathBuf>,
pub channel: String,
pub remote_match: Option<String>,
pub pile: PathBuf,
pub pre_push: bool,
pub post_commit: bool,
}
#[derive(Debug)]
pub struct SweepRepository {
pub path: PathBuf,
pub slug: String,
pub visibility: String,
pub range: String,
pub commits: usize,
pub terms: BTreeMap<String, usize>,
pub unsafe_attributes: usize,
}
#[derive(Debug)]
pub struct SweepReport {
pub channel: String,
pub protected_terms: usize,
pub root: PathBuf,
pub repos: usize,
pub gh_available: bool,
pub include_private: bool,
pub history: bool,
pub lexical_checked: bool,
pub examined: Vec<SweepRepository>,
pub skipped_private: usize,
pub no_remote: usize,
}
impl SweepReport {
pub fn flagged(&self) -> usize {
self.examined
.iter()
.filter(|repo| !repo.terms.is_empty() || repo.unsafe_attributes > 0)
.count()
}
pub fn blocked(&self) -> bool {
self.flagged() > 0 || !self.lexical_checked
}
}
#[derive(Debug)]
pub struct SemanticHit {
pub score: f32,
pub name: String,
pub line: usize,
pub snippet: String,
}
#[derive(Debug)]
pub struct SemanticReport {
pub channel: String,
pub protected: usize,
pub benign: usize,
pub threshold: f32,
pub examined: usize,
pub chunks: usize,
pub skipped: usize,
pub too_big: usize,
pub omissions: Vec<WalkOmission>,
pub hits: Vec<SemanticHit>,
}
pub const SEMANTIC_MAX_BYTES: usize = 4 * 1024 * 1024;
#[cfg(feature = "local-embed")]
enum SemanticDocument {
Text(PathBuf, String),
TooBig,
Skipped,
Omitted(WalkOmission),
}
#[derive(Clone, Debug)]
pub struct Posture {
storage: crate::storage::Storage,
}
impl Posture {
pub fn new(pile: PathBuf, key: Option<PathBuf>) -> Self {
Self::with_storage(crate::storage::Storage::new(pile, key))
}
pub fn with_storage(storage: crate::storage::Storage) -> Self {
Self { storage }
}
fn storage(&self) -> PostureStorage<'_> {
PostureStorage {
storage: &self.storage,
}
}
fn with_operation<T>(&self, operation: impl FnOnce(PostureStorage<'_>) -> Result<T>) -> Result<T> {
self.storage.scope(|storage| operation(PostureStorage { storage }))
}
pub fn scan(
&self,
label: &str,
documents: &[DocumentInput<'_>],
dry_run: bool,
) -> Result<ScanReport> {
validate_documents(label, documents.iter().map(|document| document.name))?;
let files = documents
.iter()
.map(|document| inspect_document(document.name, document.bytes))
.collect();
self.with_operation(|storage| finish_scan(storage, label, files, Vec::new(), dry_run))
}
pub fn scan_path(&self, path: &Path, dry_run: bool) -> Result<ScanReport> {
self.with_operation(|storage| cmd_scan(storage, path, dry_run))
}
pub fn list(&self, options: ListOptions) -> Result<FindingList> {
self.with_operation(|storage| cmd_list(
storage,
options.scan.map(fmt_id),
options.examples,
options.include_resolved,
))
}
pub fn coverage(&self, scan: Option<Id>) -> Result<Option<Coverage>> {
self.with_operation(|storage| cmd_coverage(storage, scan.map(fmt_id)))
}
pub fn scans(&self) -> Result<Vec<ScanRecord>> {
self.with_operation(cmd_scans)
}
pub fn vocab_add(&self, term: &str, channel: &str, why: Option<&str>) -> Result<PolicyReceipt> {
self.with_operation(|storage| cmd_vocab_add(storage, term, channel, why))
}
pub fn vocab_list(&self, channel: Option<&str>) -> Result<Vec<VocabularyChannel>> {
self.with_operation(|storage| cmd_vocab_list(storage, channel))
}
pub fn exemplar(&self, text: &str, channel: &str, benign: bool) -> Result<PolicyReceipt> {
self.with_operation(|storage| cmd_exemplar(storage, text, channel, benign))
}
pub fn semantic(
&self,
documents: &[TextInput<'_>],
channel: &str,
threshold: f32,
) -> Result<SemanticReport> {
validate_semantic(
documents.iter().map(|document| document.name),
channel,
threshold,
)?;
#[cfg(feature = "local-embed")]
{
let texts = documents.iter().map(|document| {
if document.text.len() > SEMANTIC_MAX_BYTES {
SemanticDocument::TooBig
} else {
SemanticDocument::Text(PathBuf::from(document.name), document.text.to_owned())
}
});
self.with_operation(|storage| semantic_texts(storage, texts, channel, threshold, Vec::new()))
}
#[cfg(not(feature = "local-embed"))]
bail!("built without the local-embed feature; the semantic tier is unavailable")
}
pub fn semantic_path(
&self,
path: &Path,
channel: &str,
threshold: f32,
) -> Result<SemanticReport> {
self.with_operation(|storage| cmd_semantic(storage, path, channel, threshold))
}
pub fn git(&self, repo: &Path, revisions: &[String], channel: &str) -> Result<GitReport> {
if revisions.is_empty() {
bail!("Git audit requires at least one revision");
}
self.with_operation(|storage| cmd_git(storage, revisions, channel, repo))
}
pub fn sweep(
&self,
root: &Path,
channel: &str,
all: bool,
history: bool,
) -> Result<SweepReport> {
self.with_operation(|storage| cmd_sweep(storage, root, channel, all, history))
}
pub fn install_hooks(
&self,
repo: &Path,
executable: &Path,
channel: &str,
remote_match: Option<&str>,
pre_push: bool,
post_commit: bool,
) -> Result<HookReport> {
self.with_operation(|storage| install_hooks(
storage,
repo,
executable,
channel,
remote_match,
pre_push,
post_commit,
))
}
}
pub fn validate_documents<'a>(label: &str, names: impl IntoIterator<Item = &'a str>) -> Result<()> {
if label.trim().is_empty() {
bail!("scan label cannot be empty");
}
let mut seen = BTreeSet::new();
for name in names {
if name.is_empty() {
bail!("document name cannot be empty");
}
if !seen.insert(name) {
bail!(
"duplicate document name {name:?}; one scan records one outcome per named document"
);
}
}
Ok(())
}
pub fn validate_semantic<'a>(
names: impl IntoIterator<Item = &'a str>,
channel: &str,
threshold: f32,
) -> Result<()> {
validate_documents("semantic", names)?;
canonical_channel(channel)?;
if !threshold.is_finite() {
bail!("semantic threshold must be finite");
}
Ok(())
}
pub fn validate_vocab(term: &str, channel: &str) -> Result<()> {
canonical_term(term)?;
canonical_channel(channel)?;
Ok(())
}
pub fn validate_channel(channel: &str) -> Result<()> {
canonical_channel(channel)?;
Ok(())
}
pub fn validate_exemplar(text: &str, channel: &str) -> Result<()> {
canonical_channel(channel)?;
if text.split_whitespace().count() < 12 {
bail!(
"exemplar is too short to embed meaningfully ({} words)",
text.split_whitespace().count()
);
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct Finding {
pub modality: Id,
pub location: Location,
pub evidence: String,
pub value: String,
pub seen_in: Option<String>,
}
fn member_found(
modality: Id,
carrier: Carrier,
field: impl Into<String>,
value: impl Into<String>,
) -> Finding {
let field = field.into();
Finding {
modality,
location: Location::field(carrier, field.clone()),
evidence: field,
value: value.into(),
seen_in: None,
}
}
fn xml_tag_texts(xml: &[u8], tag: &str) -> Vec<String> {
use quick_xml::events::Event;
let mut reader = quick_xml::Reader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut out = Vec::new();
let mut buf = Vec::new();
let mut depth = 0usize;
let mut current = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
if local_name(e.name().as_ref()) == tag.as_bytes() {
depth += 1;
current.clear();
}
}
Ok(Event::Text(t)) if depth > 0 => {
if let Ok(s) = t.unescape() {
current.push_str(&s);
}
}
Ok(Event::End(e)) => {
if local_name(e.name().as_ref()) == tag.as_bytes() && depth > 0 {
depth -= 1;
let s = current.trim();
if !s.is_empty() {
out.push(s.to_string());
}
current.clear();
}
}
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
buf.clear();
}
out
}
fn xml_attrs(xml: &[u8], tag: &str, attr: &str) -> Vec<(String, String)> {
use quick_xml::events::Event;
let mut reader = quick_xml::Reader::from_reader(xml);
let mut out = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
if local_name(e.name().as_ref()) == tag.as_bytes() {
let mut name = String::new();
let mut hit: Option<String> = None;
for a in e.attributes().flatten() {
let k = local_name(a.key.as_ref()).to_vec();
let v = String::from_utf8_lossy(&a.value).to_string();
if k == b"name" {
name = v.clone();
}
if k == attr.as_bytes() {
hit = Some(v);
}
}
if let Some(v) = hit {
out.push((name, v));
}
}
}
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
buf.clear();
}
out
}
fn local_name(qname: &[u8]) -> &[u8] {
match qname.iter().position(|b| *b == b':') {
Some(i) => &qname[i + 1..],
None => qname,
}
}
fn extract_ooxml(bytes: &[u8]) -> Result<Vec<Finding>> {
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
let mut out = Vec::new();
let names: Vec<String> = (0..zip.len())
.filter_map(|i| zip.by_index(i).ok().map(|f| f.name().to_string()))
.collect();
let read = |zip: &mut zip::ZipArchive<std::io::Cursor<&[u8]>>, name: &str| -> Option<Vec<u8>> {
let mut e = zip.by_name(name).ok()?;
let mut v = Vec::new();
e.read_to_end(&mut v).ok()?;
Some(v)
};
if let Some(xml) = read(&mut zip, "docProps/core.xml") {
let carrier = Carrier::member(&xml);
for tag in ["creator", "lastModifiedBy", "revision", "lastPrinted"] {
for v in xml_tag_texts(&xml, tag) {
out.push(member_found(
modality::OOXML_CORE_PROPS,
carrier.clone(),
format!("docProps/core.xml:{tag}"),
v,
));
}
}
}
if let Some(xml) = read(&mut zip, "docProps/app.xml") {
let carrier = Carrier::member(&xml);
for tag in ["Company", "Manager"] {
for v in xml_tag_texts(&xml, tag) {
out.push(member_found(
modality::OOXML_CORE_PROPS,
carrier.clone(),
format!("docProps/app.xml:{tag}"),
v,
));
}
}
}
for name in &names {
if name.contains("comments") && name.ends_with(".xml") {
if let Some(xml) = read(&mut zip, name) {
let carrier = Carrier::member(&xml);
for (_, author) in xml_attrs(&xml, "comment", "author") {
out.push(member_found(
modality::OOXML_COMMENTS,
carrier.clone(),
format!("{name}:author"),
author,
));
}
for t in xml_tag_texts(&xml, "t") {
out.push(member_found(
modality::OOXML_COMMENTS,
carrier.clone(),
format!("{name}:text"),
t,
));
}
}
}
if name.starts_with("ppt/notesSlides/") && name.ends_with(".xml") {
if let Some(xml) = read(&mut zip, name) {
let text = xml_tag_texts(&xml, "t").join(" ");
if !text.trim().is_empty() {
out.push(member_found(
modality::OOXML_SPEAKER_NOTES,
Carrier::member(&xml),
name.clone(),
text,
));
}
}
}
}
if let Some(xml) = read(&mut zip, "word/document.xml") {
let carrier = Carrier::member(&xml);
for (tag, kind) in [("ins", "insertion"), ("del", "deletion")] {
for (_, author) in xml_attrs(&xml, tag, "author") {
out.push(member_found(
modality::OOXML_TRACKED_CHANGES,
carrier.clone(),
format!("word/document.xml:{kind}@author"),
author,
));
}
}
for t in xml_tag_texts(&xml, "delText") {
out.push(member_found(
modality::OOXML_TRACKED_CHANGES,
carrier.clone(),
"word/document.xml:deleted-text",
t,
));
}
}
if let Some(xml) = read(&mut zip, "xl/workbook.xml") {
let carrier = Carrier::member(&xml);
for (name, state) in xml_attrs(&xml, "sheet", "state") {
if state == "hidden" || state == "veryHidden" {
out.push(member_found(
modality::OOXML_HIDDEN_SHEET,
carrier.clone(),
format!("xl/workbook.xml:{name}"),
format!("state={state}"),
));
}
}
}
Ok(out)
}
fn extract_exif(bytes: &[u8]) -> Result<Vec<Finding>> {
let mut reader = std::io::Cursor::new(bytes);
let exif = match exif::Reader::new().read_from_container(&mut reader) {
Ok(e) => e,
Err(exif::Error::NotFound(_)) | Err(exif::Error::BlankValue(_)) => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
let mut out = Vec::new();
let carrier = Carrier::member(exif.buf());
for field in exif.fields() {
let tag = field.tag;
let interesting = matches!(
tag,
exif::Tag::GPSLatitude
| exif::Tag::GPSLongitude
| exif::Tag::GPSAltitude
| exif::Tag::GPSDateStamp
| exif::Tag::BodySerialNumber
| exif::Tag::LensSerialNumber
| exif::Tag::CameraOwnerName
| exif::Tag::Artist
| exif::Tag::Copyright
| exif::Tag::Software
| exif::Tag::DateTimeOriginal
| exif::Tag::ImageDescription
| exif::Tag::MakerNote
);
if interesting {
let v = field.display_value().with_unit(&exif).to_string();
let v = if v.len() > 400 {
let mut end = 400;
while !v.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &v[..end])
} else {
v
};
out.push(member_found(
modality::EXIF,
carrier.clone(),
format!("EXIF:{tag}"),
v,
));
}
}
Ok(out)
}
const PDF_STREAM_LIMIT: usize = 64 << 20;
const PDF_MAX_GLYPHS: usize = 400_000;
const PDF_COVER_TOL: f32 = 1.0;
const PDF_MAX_FORM_DEPTH: usize = 6;
const PDF_CLIP_PARTS: usize = 8;
#[derive(Clone, Copy)]
struct Mat([f32; 6]);
impl Mat {
const ID: Mat = Mat([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
fn translate(x: f32, y: f32) -> Mat {
Mat([1.0, 0.0, 0.0, 1.0, x, y])
}
fn then(self, m: Mat) -> Mat {
let (a, b) = (self.0, m.0);
Mat([
a[0] * b[0] + a[1] * b[2],
a[0] * b[1] + a[1] * b[3],
a[2] * b[0] + a[3] * b[2],
a[2] * b[1] + a[3] * b[3],
a[4] * b[0] + a[5] * b[2] + b[4],
a[4] * b[1] + a[5] * b[3] + b[5],
])
}
fn apply(self, x: f32, y: f32) -> (f32, f32) {
let a = self.0;
(a[0] * x + a[2] * y + a[4], a[1] * x + a[3] * y + a[5])
}
}
#[derive(Clone, Copy)]
struct Rect {
x0: f32,
y0: f32,
x1: f32,
y1: f32,
}
impl Rect {
fn hull(pts: &[(f32, f32)]) -> Rect {
let mut r = Rect {
x0: f32::MAX,
y0: f32::MAX,
x1: f32::MIN,
y1: f32::MIN,
};
for (x, y) in pts {
r.x0 = r.x0.min(*x);
r.y0 = r.y0.min(*y);
r.x1 = r.x1.max(*x);
r.y1 = r.y1.max(*y);
}
r
}
const UNBOUNDED: Rect = Rect {
x0: -1.0e7,
y0: -1.0e7,
x1: 1.0e7,
y1: 1.0e7,
};
fn covers(&self, o: &Rect) -> bool {
o.x0 >= self.x0 - PDF_COVER_TOL
&& o.x1 <= self.x1 + PDF_COVER_TOL
&& o.y0 >= self.y0 - PDF_COVER_TOL
&& o.y1 <= self.y1 + PDF_COVER_TOL
}
fn meet(self, o: Rect) -> Rect {
Rect {
x0: self.x0.max(o.x0),
y0: self.y0.max(o.y0),
x1: self.x1.min(o.x1),
y1: self.y1.min(o.y1),
}
}
fn join(self, o: Rect) -> Rect {
Rect {
x0: self.x0.min(o.x0),
y0: self.y0.min(o.y0),
x1: self.x1.max(o.x1),
y1: self.y1.max(o.y1),
}
}
fn is_finite(&self) -> bool {
self.x0.is_finite() && self.y0.is_finite() && self.x1.is_finite() && self.y1.is_finite()
}
fn is_empty(&self) -> bool {
self.x1 <= self.x0 || self.y1 <= self.y0
}
}
#[derive(Clone, Copy)]
struct Clip {
parts: [Rect; PDF_CLIP_PARTS],
len: usize,
}
impl Clip {
const UNBOUNDED: Clip = Clip {
parts: [Rect::UNBOUNDED; PDF_CLIP_PARTS],
len: 1,
};
fn parts(&self) -> &[Rect] {
&self.parts[..self.len]
}
fn meet(self, other: &[Rect]) -> Clip {
let mut found: Vec<Rect> = Vec::new();
for a in self.parts() {
for b in other {
let m = a.meet(*b);
if m.is_finite() && !m.is_empty() {
found.push(m);
}
}
}
let mut clip = Clip {
parts: [Rect::UNBOUNDED; PDF_CLIP_PARTS],
len: 0,
};
if found.len() > PDF_CLIP_PARTS {
let hull = found.iter().copied().reduce(Rect::join).expect("non-empty");
found = vec![hull];
}
for (slot, r) in clip.parts.iter_mut().zip(found) {
*slot = r;
clip.len += 1;
}
clip
}
}
#[derive(Clone, Copy)]
struct GState {
ctm: Mat,
fill_alpha: f32,
plain_blend: bool,
clip: Clip,
}
struct Cover {
rect: Rect,
order: usize,
}
struct Glyph {
bbox: Rect,
order: usize,
size: f32,
text: String,
}
struct FontInfo<'a> {
code_bytes: usize,
widths: BTreeMap<u32, f32>,
default_width: f32,
encoding: Option<lopdf::Encoding<'a>>,
}
impl FontInfo<'_> {
fn width(&self, code: u32) -> f32 {
self.widths
.get(&code)
.copied()
.unwrap_or(self.default_width)
}
}
struct Res<'a> {
fonts: BTreeMap<Vec<u8>, FontInfo<'a>>,
xobjects: Vec<&'a Dictionary>,
extgstates: Vec<&'a Dictionary>,
}
fn deref<'a>(doc: &'a Document, obj: &'a Object) -> &'a Object {
doc.dereference(obj).map(|(_, o)| o).unwrap_or(obj)
}
fn subdicts<'a>(doc: &'a Document, chain: &[&'a Dictionary], key: &[u8]) -> Vec<&'a Dictionary> {
chain
.iter()
.filter_map(|d| d.get(key).ok())
.filter_map(|o| deref(doc, o).as_dict().ok())
.collect()
}
fn lookup<'a>(dicts: &[&'a Dictionary], name: &[u8]) -> Option<&'a Object> {
dicts.iter().find_map(|d| d.get(name).ok())
}
fn font_widths(doc: &Document, font: &Dictionary, composite: bool) -> (BTreeMap<u32, f32>, f32) {
let mut widths = BTreeMap::new();
if composite {
let desc = font
.get(b"DescendantFonts")
.ok()
.map(|o| deref(doc, o))
.and_then(|o| o.as_array().ok())
.and_then(|a| a.first())
.map(|o| deref(doc, o))
.and_then(|o| o.as_dict().ok());
let Some(desc) = desc else {
return (widths, 1000.0);
};
let default = desc
.get(b"DW")
.ok()
.and_then(|o| o.as_float().ok())
.unwrap_or(1000.0);
if let Some(w) = desc
.get(b"W")
.ok()
.map(|o| deref(doc, o))
.and_then(|o| o.as_array().ok())
{
let mut i = 0usize;
while i < w.len() {
let Ok(first) = w[i].as_float() else { break };
match w.get(i + 1).map(|o| deref(doc, o)) {
Some(Object::Array(list)) => {
for (k, item) in list.iter().enumerate() {
if let Ok(v) = item.as_float() {
widths.insert(first as u32 + k as u32, v);
}
}
i += 2;
}
Some(_) => {
let last = w[i + 1].as_float().unwrap_or(first);
let v = w
.get(i + 2)
.and_then(|o| o.as_float().ok())
.unwrap_or(default);
let last = last.min(first + 65_535.0);
let mut c = first as u32;
while c <= last as u32 {
widths.insert(c, v);
c += 1;
}
i += 3;
}
None => break,
}
}
}
return (widths, default);
}
let first = font
.get(b"FirstChar")
.ok()
.and_then(|o| o.as_i64().ok())
.unwrap_or(0);
if let Some(list) = font
.get(b"Widths")
.ok()
.map(|o| deref(doc, o))
.and_then(|o| o.as_array().ok())
{
for (k, item) in list.iter().enumerate() {
if let Ok(v) = deref(doc, item).as_float() {
widths.insert((first + k as i64).max(0) as u32, v);
}
}
}
let missing = font
.get(b"FontDescriptor")
.ok()
.map(|o| deref(doc, o))
.and_then(|o| o.as_dict().ok())
.and_then(|d| d.get(b"MissingWidth").ok())
.and_then(|o| o.as_float().ok())
.unwrap_or(500.0);
(widths, missing)
}
fn build_res<'a>(doc: &'a Document, chain: &[&'a Dictionary]) -> Res<'a> {
let mut fonts: BTreeMap<Vec<u8>, FontInfo<'a>> = BTreeMap::new();
for fdict in subdicts(doc, chain, b"Font") {
for (name, value) in fdict.iter() {
if fonts.contains_key(name) {
continue;
}
let Ok(font) = deref(doc, value).as_dict() else {
continue;
};
let composite = font
.get(b"Subtype")
.and_then(|o| o.as_name())
.map(|n| n == b"Type0")
.unwrap_or(false);
let (widths, default_width) = font_widths(doc, font, composite);
fonts.insert(
name.clone(),
FontInfo {
code_bytes: if composite { 2 } else { 1 },
widths,
default_width,
encoding: font
.get_font_encoding_with_limit(doc, PDF_STREAM_LIMIT)
.ok(),
},
);
}
}
Res {
fonts,
xobjects: subdicts(doc, chain, b"XObject"),
extgstates: subdicts(doc, chain, b"ExtGState"),
}
}
fn opnum(op: &lopdf::content::Operation, i: usize) -> f32 {
op.operands
.get(i)
.and_then(|o| o.as_float().ok())
.unwrap_or(0.0)
}
fn opmat(op: &lopdf::content::Operation) -> Mat {
Mat([
opnum(op, 0),
opnum(op, 1),
opnum(op, 2),
opnum(op, 3),
opnum(op, 4),
opnum(op, 5),
])
}
#[derive(Clone, Copy)]
struct TState<'r, 'a> {
tm: Mat,
tlm: Mat,
font: Option<&'r FontInfo<'a>>,
size: f32,
tc: f32,
tw: f32,
th: f32,
tl: f32,
ts: f32,
}
impl TState<'_, '_> {
fn new() -> Self {
TState {
tm: Mat::ID,
tlm: Mat::ID,
font: None,
size: 0.0,
tc: 0.0,
tw: 0.0,
th: 1.0,
tl: 0.0,
ts: 0.0,
}
}
fn line(&mut self, tx: f32, ty: f32) {
self.tlm = Mat::translate(tx, ty).then(self.tlm);
self.tm = self.tlm;
}
}
struct PageScan<'a> {
doc: &'a Document,
order: usize,
covers: Vec<Cover>,
glyphs: Vec<Glyph>,
}
impl<'a> PageScan<'a> {
fn stream(&mut self, content: &[u8], res: &Res<'a>, base: GState, depth: usize) -> Result<()> {
let doc = self.doc;
let ops = lopdf::content::Content::decode(content)
.map_err(|e| anyhow!("content stream undecodable: {e}"))?
.operations;
let mut gs = base;
let mut stack: Vec<(GState, TState)> = Vec::new();
let mut boxes: Vec<Rect> = Vec::new();
let mut pts: Vec<(f32, f32)> = Vec::new();
let mut sub: Vec<(f32, f32)> = Vec::new();
let mut sub_curved = false;
let mut irregular = false;
let mut pending_clip = false;
let mut t = TState::new();
for op in &ops {
self.order += 1;
match op.operator.as_str() {
"q" => stack.push((gs, t)),
"Q" => {
if let Some((saved_gs, saved_t)) = stack.pop() {
gs = saved_gs;
let (tm, tlm) = (t.tm, t.tlm);
t = saved_t;
t.tm = tm;
t.tlm = tlm;
}
}
"cm" => gs.ctm = opmat(op).then(gs.ctm),
"gs" => {
let named = op
.operands
.first()
.and_then(|o| o.as_name().ok())
.and_then(|n| lookup(&res.extgstates, n))
.map(|o| deref(doc, o))
.and_then(|o| o.as_dict().ok());
if let Some(d) = named {
if let Some(ca) = d.get(b"ca").ok().and_then(|o| o.as_float().ok()) {
gs.fill_alpha = ca;
}
if let Ok(bm) = d.get(b"BM") {
let name = match deref(doc, bm) {
Object::Name(n) => n.clone(),
Object::Array(a) => a
.first()
.and_then(|o| o.as_name().ok())
.map(|n| n.to_vec())
.unwrap_or_default(),
_ => Vec::new(),
};
gs.plain_blend =
name.is_empty() || name == b"Normal" || name == b"Compatible";
}
}
}
"re" => {
let (x, y, w, h) = (opnum(op, 0), opnum(op, 1), opnum(op, 2), opnum(op, 3));
let corners = [
gs.ctm.apply(x, y),
gs.ctm.apply(x + w, y),
gs.ctm.apply(x, y + h),
gs.ctm.apply(x + w, y + h),
];
boxes.push(Rect::hull(&corners));
pts.extend_from_slice(&corners);
}
"m" => {
irregular |= !flush_subpath(&mut sub, &mut sub_curved, &mut boxes);
let p = gs.ctm.apply(opnum(op, 0), opnum(op, 1));
sub.push(p);
pts.push(p);
}
"l" => {
let p = gs.ctm.apply(opnum(op, 0), opnum(op, 1));
sub.push(p);
pts.push(p);
}
"c" | "v" | "y" => {
sub_curved = true;
for i in (0..op.operands.len().saturating_sub(1)).step_by(2) {
let p = gs.ctm.apply(opnum(op, i), opnum(op, i + 1));
pts.push(p);
sub.push(p);
}
}
"h" => {}
"W" | "W*" => pending_clip = true,
"f" | "F" | "f*" | "b" | "b*" | "B" | "B*" | "S" | "s" | "n" => {
irregular |= !flush_subpath(&mut sub, &mut sub_curved, &mut boxes);
let fills = !matches!(op.operator.as_str(), "S" | "s" | "n");
if fills && gs.fill_alpha >= 0.99 && gs.plain_blend {
for r in &boxes {
for part in gs.clip.parts() {
let r = r.meet(*part);
if r.is_finite() && !r.is_empty() {
self.covers.push(Cover {
rect: r,
order: self.order,
});
}
}
}
}
if pending_clip && !pts.is_empty() {
let hull = [Rect::hull(&pts)];
let region: &[Rect] = if irregular || boxes.is_empty() {
&hull
} else {
&boxes
};
gs.clip = gs.clip.meet(region);
}
boxes.clear();
pts.clear();
sub.clear();
sub_curved = false;
irregular = false;
pending_clip = false;
}
"BT" => {
t.tm = Mat::ID;
t.tlm = Mat::ID;
}
"Tf" => {
t.font = op
.operands
.first()
.and_then(|o| o.as_name().ok())
.and_then(|n| res.fonts.get(n));
t.size = opnum(op, 1);
}
"Td" => t.line(opnum(op, 0), opnum(op, 1)),
"TD" => {
t.tl = -opnum(op, 1);
t.line(opnum(op, 0), opnum(op, 1));
}
"Tm" => {
t.tlm = opmat(op);
t.tm = t.tlm;
}
"T*" => t.line(0.0, -t.tl),
"TL" => t.tl = opnum(op, 0),
"Tc" => t.tc = opnum(op, 0),
"Tw" => t.tw = opnum(op, 0),
"Tz" => t.th = opnum(op, 0) / 100.0,
"Ts" => t.ts = opnum(op, 0),
"Tj" => {
if let Some(bytes) = op.operands.first().and_then(|o| o.as_str().ok()) {
self.show(bytes, &mut t, gs.ctm);
}
}
"'" | "\"" => {
let bytes = if op.operator == "\"" {
t.tw = opnum(op, 0);
t.tc = opnum(op, 1);
op.operands.get(2)
} else {
op.operands.first()
};
t.line(0.0, -t.tl);
if let Some(bytes) = bytes.and_then(|o| o.as_str().ok()) {
self.show(bytes, &mut t, gs.ctm);
}
}
"TJ" => {
let items = op.operands.first().and_then(|o| o.as_array().ok());
for item in items.into_iter().flatten() {
match item {
Object::String(bytes, _) => self.show(bytes, &mut t, gs.ctm),
_ => {
if let Ok(n) = item.as_float() {
t.tm =
Mat::translate(-n / 1000.0 * t.size * t.th, 0.0).then(t.tm);
}
}
}
}
}
"Do" if depth < PDF_MAX_FORM_DEPTH => {
let stream = op
.operands
.first()
.and_then(|o| o.as_name().ok())
.and_then(|n| lookup(&res.xobjects, n))
.map(|o| deref(doc, o))
.and_then(|o| o.as_stream().ok());
let Some(stream) = stream else { continue };
if stream.dict.get(b"Subtype").and_then(|o| o.as_name()).ok() != Some(b"Form") {
continue;
}
let Ok(inner) = stream.get_plain_content_with_limit(PDF_STREAM_LIMIT) else {
continue;
};
let matrix = stream
.dict
.get(b"Matrix")
.ok()
.and_then(|o| o.as_array().ok())
.filter(|a| a.len() == 6)
.map(|a| {
let mut m = [0.0f32; 6];
for (k, slot) in m.iter_mut().enumerate() {
*slot = a[k].as_float().unwrap_or(if k == 0 || k == 3 {
1.0
} else {
0.0
});
}
Mat(m)
})
.unwrap_or(Mat::ID);
let ctm = matrix.then(gs.ctm);
let bbox = stream
.dict
.get(b"BBox")
.ok()
.and_then(|o| deref(doc, o).as_array().ok())
.filter(|a| a.len() == 4)
.map(|a| {
let v: Vec<f32> =
a.iter().map(|o| o.as_float().unwrap_or(0.0)).collect();
Rect::hull(&[
ctm.apply(v[0], v[1]),
ctm.apply(v[2], v[1]),
ctm.apply(v[0], v[3]),
ctm.apply(v[2], v[3]),
])
})
.unwrap_or(Rect::UNBOUNDED);
let inner_gs = GState {
ctm,
clip: gs.clip.meet(&[bbox]),
..gs
};
let own = stream
.dict
.get(b"Resources")
.ok()
.map(|o| deref(doc, o))
.and_then(|o| o.as_dict().ok());
match own {
Some(d) => {
let inner_res = build_res(doc, &[d]);
self.stream(&inner, &inner_res, inner_gs, depth + 1)?;
}
None => self.stream(&inner, res, inner_gs, depth + 1)?,
}
}
_ => {}
}
}
Ok(())
}
fn show(&mut self, bytes: &[u8], t: &mut TState, ctm: Mat) {
let Some(font) = t.font else {
t.tm = Mat::translate(bytes.len() as f32 * 0.5 * t.size * t.th, 0.0).then(t.tm);
return;
};
let (up, down) = (0.70 * t.size, 0.20 * t.size);
for chunk in bytes.chunks(font.code_bytes) {
let mut code = 0u32;
for b in chunk {
code = code * 256 + *b as u32;
}
let w = font.width(code) / 1000.0 * t.size * t.th;
if self.glyphs.len() < PDF_MAX_GLYPHS {
let trm = t.tm.then(ctm);
let bbox = Rect::hull(&[
trm.apply(0.0, t.ts - down),
trm.apply(w, t.ts - down),
trm.apply(0.0, t.ts + up),
trm.apply(w, t.ts + up),
]);
let text = font
.encoding
.as_ref()
.and_then(|e| e.bytes_to_string(chunk).ok())
.unwrap_or_else(|| "\u{FFFD}".to_string());
if bbox.is_finite() && !text.is_empty() {
self.glyphs.push(Glyph {
bbox,
order: self.order,
size: t.size,
text,
});
}
}
let word = if font.code_bytes == 1 && code == 32 {
t.tw
} else {
0.0
};
t.tm = Mat::translate(
(font.width(code) / 1000.0 * t.size + t.tc + word) * t.th,
0.0,
)
.then(t.tm);
}
}
fn report(&self, page: u32, carrier: &Carrier, out: &mut Vec<Finding>) {
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for cover in &self.covers {
let mut text = String::new();
let mut prev: Option<&Glyph> = None;
for g in self
.glyphs
.iter()
.filter(|g| g.order < cover.order && cover.rect.covers(&g.bbox))
{
if let Some(p) = prev {
let newline = (g.bbox.y0 - p.bbox.y0).abs() > 0.5 * g.size;
if (newline || g.bbox.x0 - p.bbox.x1 > 0.18 * g.size) && !text.ends_with(' ') {
text.push(' ');
}
}
text.push_str(&g.text);
prev = Some(g);
}
let text = text.trim();
if text.is_empty() {
continue;
}
let r = &cover.rect;
let locator = format!(
"page {page}:box({:.0},{:.0} {:.0}x{:.0})",
r.x0,
r.y0,
r.x1 - r.x0,
r.y1 - r.y0
);
if !seen.insert(format!("{locator}\u{0}{text}")) {
continue;
}
let value = if text.chars().count() > 1000 {
format!("{}…", text.chars().take(1000).collect::<String>())
} else {
text.to_string()
};
out.push(member_found(
modality::PDF_REDACTION_RECT,
carrier.clone(),
locator,
value,
));
}
}
}
fn polygon_fills_its_bbox(pts: &[(f32, f32)], min_ratio: f32) -> Option<Rect> {
if pts.len() < 3 {
return None;
}
let hull = Rect::hull(pts);
let box_area = (hull.x1 - hull.x0) * (hull.y1 - hull.y0);
if !box_area.is_finite() || box_area <= 0.0 {
return None;
}
let mut twice = 0.0f32;
for i in 0..pts.len() {
let (x0, y0) = pts[i];
let (x1, y1) = pts[(i + 1) % pts.len()];
twice += x0 * y1 - x1 * y0;
}
let area = (twice * 0.5).abs();
(area / box_area >= min_ratio).then_some(hull)
}
fn flush_subpath(sub: &mut Vec<(f32, f32)>, curved: &mut bool, boxes: &mut Vec<Rect>) -> bool {
let pts = std::mem::take(sub);
let was_curved = std::mem::replace(curved, false);
if pts.is_empty() {
return true;
}
if was_curved {
return polygon_fills_its_bbox(&pts, 0.97)
.inspect(|r| boxes.push(*r))
.is_some();
}
let n = if pts.len() == 5 && pts[0] == pts[4] {
4
} else {
pts.len()
};
if n != 4 {
return false;
}
let axis_aligned = (0..4).all(|i| {
let a = pts[i];
let b = pts[(i + 1) % 4];
(a.0 - b.0).abs() < 0.01 || (a.1 - b.1).abs() < 0.01
});
if axis_aligned {
boxes.push(Rect::hull(&pts[..4]));
}
axis_aligned
}
const PDF_INFO_KEYS: &[&str] = &[
"Author",
"Creator",
"Producer",
"Title",
"Subject",
"Keywords",
"CreationDate",
"ModDate",
];
const PDF_XMP_FIELDS: &[&str] = &[
"creator",
"title",
"description",
"rights",
"CreatorTool",
"CreateDate",
"ModifyDate",
"MetadataDate",
"Producer",
"Keywords",
"Company",
"Author",
"AuthorsPosition",
"Credit",
"Source",
"DocumentID",
"InstanceID",
"OriginalDocumentID",
];
fn pdf_value_text(doc: &Document, obj: &Object) -> Option<String> {
let obj = deref(doc, obj);
let s = match obj {
Object::String(..) => lopdf::decode_text_string(obj).ok()?,
Object::Name(n) => String::from_utf8_lossy(n).to_string(),
Object::Integer(i) => i.to_string(),
Object::Real(r) => r.to_string(),
Object::Boolean(b) => b.to_string(),
_ => return None,
};
let s = s.trim().to_string();
if s.is_empty() {
None
} else {
Some(s)
}
}
fn extract_pdf_metadata(doc: &Document, file: &Carrier, out: &mut Vec<Finding>) {
if let Some(info) = doc
.trailer
.get(b"Info")
.ok()
.map(|o| deref(doc, o))
.and_then(|o| o.as_dict().ok())
{
for key in PDF_INFO_KEYS {
if let Some(v) = info
.get(key.as_bytes())
.ok()
.and_then(|o| pdf_value_text(doc, o))
{
out.push(member_found(
modality::PDF_METADATA,
file.clone(),
format!("Info:{key}"),
v,
));
}
}
for (name, value) in info.iter() {
let name = String::from_utf8_lossy(name).to_string();
if PDF_INFO_KEYS.contains(&name.as_str()) || name == "Trapped" {
continue;
}
if let Some(v) = pdf_value_text(doc, value) {
out.push(member_found(
modality::PDF_METADATA,
file.clone(),
format!("Info:{name}"),
v,
));
}
}
}
let xmp = doc
.catalog()
.ok()
.and_then(|c| c.get(b"Metadata").ok())
.map(|o| deref(doc, o))
.and_then(|o| o.as_stream().ok())
.and_then(|s| s.get_plain_content_with_limit(PDF_STREAM_LIMIT).ok());
let Some(xmp) = xmp else { return };
let packet = Carrier::member(&xmp);
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for field in PDF_XMP_FIELDS {
let mut values = xml_tag_texts(&xmp, field);
values.extend(
xml_attrs(&xmp, "Description", field)
.into_iter()
.map(|(_, v)| v),
);
for v in values {
let v = v.trim().to_string();
if v.is_empty() || !seen.insert(format!("{field}\u{0}{v}")) {
continue;
}
let v = if v.chars().count() > 400 {
format!("{}…", v.chars().take(400).collect::<String>())
} else {
v
};
out.push(member_found(
modality::PDF_METADATA,
packet.clone(),
format!("XMP:{field}"),
v,
));
}
}
}
fn extract_pdf(bytes: &[u8]) -> Result<Vec<Finding>> {
let doc = Document::load_mem_with_options(
bytes,
lopdf::LoadOptions {
max_decompressed_size: Some(PDF_STREAM_LIMIT),
..Default::default()
},
)
.map_err(|e| anyhow!("load pdf: {e}"))?;
let mut out = Vec::new();
let file = Carrier::member(bytes);
extract_pdf_metadata(&doc, &file, &mut out);
for (page_no, page_id) in doc.get_pages() {
let content = doc
.get_page_content_with_limit(page_id, PDF_STREAM_LIMIT)
.map_err(|e| anyhow!("page {page_no}: content unreadable ({e}) — page NOT examined"))?;
let chain = resource_chain(&doc, page_id);
let res = build_res(&doc, &chain);
let mut scan = PageScan {
doc: &doc,
order: 0,
covers: Vec::new(),
glyphs: Vec::new(),
};
let base = GState {
ctm: Mat::ID,
fill_alpha: 1.0,
plain_blend: true,
clip: Clip::UNBOUNDED,
};
scan.stream(&content, &res, base, 0)
.map_err(|e| anyhow!("page {page_no}: {e}"))?;
scan.report(page_no, &Carrier::member(&content), &mut out);
}
Ok(out)
}
fn resource_chain<'a>(doc: &'a Document, page_id: lopdf::ObjectId) -> Vec<&'a Dictionary> {
let mut chain = Vec::new();
if let Ok((direct, inherited)) = doc.get_page_resources(page_id) {
if let Some(d) = direct {
chain.push(d);
}
for id in inherited {
if let Ok(d) = doc.get_dictionary(id) {
chain.push(d);
}
}
}
chain
}
fn dispatch(name: &str, bytes: &[u8]) -> Option<fn(&[u8]) -> Result<Vec<Finding>>> {
if bytes.starts_with(b"%PDF") {
return Some(extract_pdf);
}
if bytes.starts_with(b"PK\x03\x04") || bytes.starts_with(b"PK\x05\x06") {
return Some(extract_ooxml);
}
if bytes.starts_with(&[0xFF, 0xD8, 0xFF])
|| bytes.starts_with(&[0x89, b'P', b'N', b'G'])
|| bytes.starts_with(b"II*\0")
|| bytes.starts_with(b"MM\0*")
|| (bytes.len() >= 12 && &bytes[4..8] == b"ftyp")
{
return Some(extract_exif);
}
let ext = Path::new(name)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
.to_ascii_lowercase();
match ext.as_str() {
"docx" | "xlsx" | "pptx" | "docm" | "xlsm" | "pptm" => Some(extract_ooxml),
"jpg" | "jpeg" | "tif" | "tiff" | "heic" | "heif" | "png" | "webp" => Some(extract_exif),
"pdf" => Some(extract_pdf),
_ => None,
}
}
pub fn inspect_document(name: &str, bytes: &[u8]) -> ScannedFile {
let (outcome, findings) = match dispatch(name, bytes) {
Some(extractor) => match extractor(bytes) {
Ok(findings) => (FileOutcome::Examined, findings),
Err(error) => (FileOutcome::ParseFailed(error.to_string()), Vec::new()),
},
None => (FileOutcome::Unsupported, Vec::new()),
};
ScannedFile {
path: PathBuf::from(name),
outcome,
findings,
}
}
fn inspect_reader(name: &str, mut reader: impl Read) -> Result<ScannedFile> {
let mut head = [0u8; 12];
let len = reader
.read(&mut head)
.context("read type-detection header")?;
if dispatch(name, &head[..len]).is_none() {
return Ok(ScannedFile {
path: PathBuf::from(name),
outcome: FileOutcome::Unsupported,
findings: Vec::new(),
});
}
let mut bytes = head[..len].to_vec();
reader
.read_to_end(&mut bytes)
.context("read supported document")?;
Ok(inspect_document(name, &bytes))
}
const IMPLEMENTED: &[Id] = &[
modality::OOXML_CORE_PROPS,
modality::OOXML_COMMENTS,
modality::OOXML_TRACKED_CHANGES,
modality::OOXML_SPEAKER_NOTES,
modality::OOXML_HIDDEN_SHEET,
modality::EXIF,
modality::PDF_METADATA,
modality::PDF_REDACTION_RECT,
];
#[derive(Clone, Debug)]
pub struct WalkOmission {
pub path: PathBuf,
pub detail: String,
}
fn walk(root: &Path, out: &mut Vec<PathBuf>, omissions: &mut Vec<WalkOmission>) {
let metadata = match std::fs::symlink_metadata(root) {
Ok(metadata) => metadata,
Err(error) => {
omissions.push(WalkOmission {
path: root.to_path_buf(),
detail: format!("metadata failed: {error}"),
});
return;
}
};
if metadata.file_type().is_symlink() {
if root.is_file() {
out.push(root.to_path_buf());
} else {
omissions.push(WalkOmission {
path: root.to_path_buf(),
detail: "symlinked directory not followed".to_owned(),
});
}
return;
}
if metadata.is_file() {
out.push(root.to_path_buf());
return;
}
let entries = match std::fs::read_dir(root) {
Ok(entries) => entries,
Err(error) => {
omissions.push(WalkOmission {
path: root.to_path_buf(),
detail: format!("directory read failed: {error}"),
});
return;
}
};
for entry in entries {
let e = match entry {
Ok(entry) => entry,
Err(error) => {
omissions.push(WalkOmission {
path: root.to_path_buf(),
detail: format!("directory entry read failed: {error}"),
});
continue;
}
};
let p = e.path();
if p.file_name().and_then(|n| n.to_str()) == Some(".git") {
continue;
}
walk(&p, out, omissions);
}
}
#[derive(Clone, Copy)]
struct PostureStorage<'a> {
storage: &'a crate::storage::Storage,
}
struct CollectionView {
facts: FactArchive,
reader: PileSnapshot,
}
impl PostureStorage<'_> {
fn load_scope(&self, scope: Id, label: &str) -> Result<CollectionView> {
self.load_scopes(&[(scope, label)])?
.pop()
.ok_or_else(|| anyhow!("missing Posture {label} collection view"))
}
fn load_scopes(&self, scopes: &[(Id, &str)]) -> Result<Vec<CollectionView>> {
self.storage.with_store(|store, signer, runtime| {
for (scope, _) in scopes {
crate::collection_names::open_configured_acquiring(
store, *scope, signer.verifying_key(), runtime,
)?;
}
let pile = store;
let result = (|| {
let mut succinct = Vec::with_capacity(scopes.len());
let mut rank9 = Vec::with_capacity(scopes.len());
for (scope, label) in scopes {
let collection = open_configured(pile, *scope, signer.verifying_key())?;
let succinct_collection = pile
.attach::<SuccinctArchiveBlob>(collection, ())
.with_context(|| format!("register succinct Posture {label} collection"))?;
let rank9_collection = pile
.attach::<Rank9AcceleratedSuccinctArchiveBlob>(
collection,
succinct_collection,
)
.with_context(|| format!("register Rank9 Posture {label} collection"))?;
succinct.push(succinct_collection);
rank9.push(rank9_collection);
}
runtime.block_on(async {
for (index, (_, label)) in scopes.iter().enumerate() {
crate::storage::tolerate_own_lag(
pile.maintain_attached(succinct[index], signer).await,
)
.with_context(|| format!("maintain succinct Posture {label} collection"))?;
crate::storage::tolerate_own_lag(
pile.maintain_attached(rank9[index], signer).await,
)
.with_context(|| format!("maintain Posture {label} collection"))?;
}
Ok::<_, anyhow::Error>(())
})?;
let reader = AcquiringReader::new(
pile.snapshot().context("freeze maintained Posture store snapshot")?,
runtime.clone(),
);
scopes
.iter()
.zip(rank9)
.map(|((_, label), collection)| {
let facts = crate::storage::acquire_facts(&reader, collection)
.with_context(|| {
format!("read maintained Posture {label} collection")
})?;
Ok(CollectionView {
facts,
reader: reader.clone(),
})
})
.collect()
})();
result
})
}
fn scan_and_decide_views(&self) -> Result<(CollectionView, CollectionView)> {
let mut views = self.load_scopes(&[
(DEFAULT_SCAN_SCOPE_ID, "scan"),
(DEFAULT_DECIDE_SCOPE_ID, "Decide"),
])?;
let decisions = views.pop().expect("two requested Posture views");
let scans = views.pop().expect("two requested Posture views");
Ok((scans, decisions))
}
fn policy_scan_and_decide_views(
&self,
) -> Result<(CollectionView, CollectionView, CollectionView)> {
let mut views = self.load_scopes(&[
(DEFAULT_POLICY_SCOPE_ID, "policy"),
(DEFAULT_SCAN_SCOPE_ID, "scan"),
(DEFAULT_DECIDE_SCOPE_ID, "Decide"),
])?;
let decisions = views.pop().expect("three requested Posture views");
let scans = views.pop().expect("three requested Posture views");
let policy = views.pop().expect("three requested Posture views");
Ok((policy, scans, decisions))
}
#[cfg(test)]
fn admitted_payloads(&self, scope: Id, label: &str) -> Result<usize> {
self.storage.with_pile(|pile, signer| {
let collection = open_configured(pile, scope, signer.verifying_key())?;
let store_snapshot = pile
.snapshot()
.with_context(|| format!("freeze admitted Posture {label} store snapshot"))?;
Ok(collection
.admitted(&store_snapshot)
.with_context(|| format!("admit Posture {label} collection"))?
.len())
})
}
fn policy_view(&self) -> Result<CollectionView> {
self.load_scope(DEFAULT_POLICY_SCOPE_ID, "policy")
}
fn scan_view(&self) -> Result<CollectionView> {
self.load_scope(DEFAULT_SCAN_SCOPE_ID, "scan")
}
#[cfg(test)]
fn decide_view(&self) -> Result<CollectionView> {
self.load_scope(DEFAULT_DECIDE_SCOPE_ID, "Decide")
}
fn publish_policy(
&self,
mut fragment: Fragment,
description: &str,
) -> Result<CollectionCommit> {
self.with_store(
DEFAULT_POLICY_SCOPE_ID,
"policy",
|pile, collection, signer, runtime| {
fragment.describe_with(entity! { metadata::description: description.to_owned() });
let commit = pile
.commit(collection, signer, fragment)
.context("commit authored Posture policy fragment")?;
drop(
runtime.block_on(crate::storage::ensure_downstream(pile, collection, signer))
.context(
"Posture policy facts were committed, but ensuring their derived views failed",
)?,
);
Ok(commit)
},
)
}
fn publish_scan(&self, mut fragment: Fragment, description: &str) -> Result<CollectionCommit> {
self.with_store(DEFAULT_SCAN_SCOPE_ID, "scan", |pile, collection, signer, runtime| {
fragment.describe_with(entity! { metadata::description: description.to_owned() });
let commit = pile
.commit(collection, signer, fragment)
.context("commit authored Posture scan fragment")?;
drop(
runtime.block_on(crate::storage::ensure_downstream(pile, collection, signer))
.context(
"Posture scan facts were committed, but ensuring their derived views failed",
)?,
);
Ok(commit)
})
}
fn with_store<T>(
&self,
scope: Id,
label: &str,
operation: impl FnOnce(
&mut crate::storage::FacultyStore,
Collection<SimpleArchive>,
&ed25519_dalek::SigningKey,
&std::sync::Arc<tokio::runtime::Runtime>,
) -> Result<T>,
) -> Result<T> {
self.storage.with_store(|store, signer, runtime| {
let collection = crate::collection_names::open_configured_acquiring(
store, scope, signer.verifying_key(), runtime,
)
.with_context(|| format!("open Posture {label} collection"))?;
let result = (|| {
operation(store, collection, signer, runtime)
})();
result
})
}
}
fn read_text(reader: &PileSnapshot, handle: TextHandle, field: &str) -> Result<String> {
let value: View<str> = reader
.get(handle)
.with_context(|| format!("read Posture {field} payload"))?;
Ok(value.to_string())
}
fn now_epoch() -> Result<Epoch> {
clock::now()
}
fn point_interval(epoch: Epoch) -> IntervalValue {
(epoch, epoch)
.try_to_inline()
.expect("valid point interval")
}
fn interval_key(interval: IntervalValue) -> i128 {
let (lower, _): (i128, i128) = interval
.try_from_inline()
.expect("valid Posture timestamp interval");
lower
}
fn one_required<T>(values: BTreeSet<T>, field: &str) -> Result<T>
where
T: Ord + Copy,
{
if values.len() != 1 {
bail!("{field} has {} values; expected exactly one", values.len());
}
Ok(*values.iter().next().expect("one value"))
}
fn one_optional<T>(values: BTreeSet<T>, field: &str) -> Result<Option<T>>
where
T: Ord + Copy,
{
if values.len() > 1 {
bail!("{field} has {} values; expected at most one", values.len());
}
Ok(values.iter().next().copied())
}
fn fmt_id(id: Id) -> String {
format!("{id:x}")
}
fn sighting_entity(fragment: &mut Fragment, finding: Id, document: Id, found: &Finding) -> Id {
let value: TextHandle = fragment.put(found.value.clone());
let evidence: TextHandle = fragment.put(found.evidence.clone());
let seen_in: Option<TextHandle> = found
.seen_in
.as_ref()
.map(|commit| fragment.put(commit.clone()));
let sighting = entity! {
metadata::tag: KIND_SIGHTING,
posture::sighting_of: finding,
posture::document: document,
posture::value: value,
posture::evidence: evidence,
posture::seen_in?: seen_in,
};
let id = sighting.root().expect("intrinsic sighting has one root");
*fragment += sighting;
id
}
#[cfg(test)]
fn entity_attributes(facts: &TribleSet, entity: Id) -> BTreeSet<Id> {
facts
.iter()
.filter(|fact| fact.e() == &entity)
.map(|fact| *fact.a())
.collect()
}
#[cfg(test)]
fn require_attributes(
facts: &TribleSet,
entity: Id,
allowed: impl IntoIterator<Item = Id>,
kind: &str,
) -> Result<()> {
let actual = entity_attributes(facts, entity);
let allowed = allowed.into_iter().collect::<BTreeSet<_>>();
if !actual.is_subset(&allowed) {
let unexpected = actual.difference(&allowed).copied().collect::<Vec<_>>();
bail!(
"{kind} {} has unexpected attribute(s): {}",
fmt_id(entity),
unexpected
.iter()
.map(|attribute| fmt_id(*attribute))
.collect::<Vec<_>>()
.join(", ")
);
}
Ok(())
}
#[cfg(test)]
fn entity_tags(facts: &TribleSet, entity: Id) -> BTreeSet<Id> {
find!(
tag: Id,
pattern!(facts, [{ (entity) @ metadata::tag: ?tag }])
)
.collect()
}
#[cfg(test)]
fn inline_u256_to_u128(value: Inline<inlineencodings::U256BE>) -> Result<u128> {
if value.raw[..16].iter().any(|byte| *byte != 0) {
bail!("Posture count exceeds u128");
}
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&value.raw[16..]);
Ok(u128::from_be_bytes(bytes))
}
#[cfg(test)]
fn validate_scan_commit_fragment(facts: &TribleSet) -> Result<Id> {
let scans = find!(
scan: Id,
pattern!(facts, [{ ?scan @ metadata::tag: (&KIND_SCAN) }])
)
.collect::<BTreeSet<_>>();
let scan = one_required(scans, "scan COMMIT root")?;
validate_scan_structure(facts)?;
let mut expected_subjects = BTreeSet::from([scan]);
expected_subjects.extend(find!(
value: Id,
pattern!(facts, [{ (scan) @ posture::scan_document: ?value }])
));
let sightings = find!(
value: Id,
pattern!(facts, [{ (scan) @ posture::scan_sighting: ?value }])
)
.collect::<BTreeSet<_>>();
for sighting in &sightings {
expected_subjects.extend(find!(
value: Id,
pattern!(facts, [{ (*sighting) @ posture::sighting_of: ?value }])
));
}
expected_subjects.extend(sightings);
expected_subjects.extend(find!(
value: Id,
pattern!(facts, [{ (scan) @ posture::scan_omission: ?value }])
));
let actual_subjects = facts.iter().map(|fact| *fact.e()).collect::<BTreeSet<_>>();
if actual_subjects != expected_subjects {
bail!("scan COMMIT must contain exactly its Merkle-rooted observation");
}
Ok(scan)
}
#[cfg(test)]
fn validate_scan_structure(facts: &TribleSet) -> Result<()> {
let scans = find!(
scan: Id,
pattern!(facts, [{ ?scan @ metadata::tag: (&KIND_SCAN) }])
)
.collect::<BTreeSet<_>>();
let documents = find!(
document: Id,
pattern!(facts, [{ ?document @ metadata::tag: (&KIND_DOCUMENT) }])
)
.collect::<BTreeSet<_>>();
let findings = find!(
finding: Id,
pattern!(facts, [{ ?finding @ metadata::tag: (&KIND_FINDING) }])
)
.collect::<BTreeSet<_>>();
let sightings = find!(
sighting: Id,
pattern!(facts, [{ ?sighting @ metadata::tag: (&KIND_SIGHTING) }])
)
.collect::<BTreeSet<_>>();
let omissions = find!(
omission: Id,
pattern!(facts, [{ ?omission @ metadata::tag: (&KIND_OMISSION) }])
)
.collect::<BTreeSet<_>>();
let mut known = scans.clone();
known.extend(documents.iter().copied());
known.extend(findings.iter().copied());
known.extend(sightings.iter().copied());
known.extend(omissions.iter().copied());
let actual = facts.iter().map(|fact| *fact.e()).collect::<BTreeSet<_>>();
if actual != known {
bail!("Posture scan collection contains unrecognized entities");
}
let coverage_modalities = modality::ALL
.iter()
.map(|(id, _)| *id)
.collect::<BTreeSet<_>>();
let known_modalities = modality::ALL
.iter()
.chain(modality::GIT_ONLY)
.map(|(id, _)| *id)
.collect::<BTreeSet<_>>();
for scan in &scans {
require_attributes(
facts,
*scan,
[
metadata::tag.id(),
metadata::created_at.id(),
posture::target.id(),
posture::file_count.id(),
posture::checked.id(),
posture::unchecked.id(),
posture::scan_channel.id(),
posture::scan_document.id(),
posture::scan_sighting.id(),
posture::scan_omission.id(),
],
"scan",
)?;
if entity_tags(facts, *scan) != BTreeSet::from([KIND_SCAN]) {
bail!("scan {} has invalid tags", fmt_id(*scan));
}
let created_at = one_required(
find!(
value: IntervalValue,
pattern!(facts, [{ (*scan) @ metadata::created_at: ?value }])
)
.collect(),
"scan created_at",
)?;
let target = one_required(
find!(value: TextHandle, pattern!(facts, [{ (*scan) @ posture::target: ?value }]))
.collect(),
"scan target",
)?;
let file_count = one_required(
find!(
value: Inline<inlineencodings::U256BE>,
pattern!(facts, [{ (*scan) @ posture::file_count: ?value }])
)
.collect(),
"scan file count",
)?;
let checked = find!(
value: Id,
pattern!(facts, [{ (*scan) @ posture::checked: ?value }])
)
.collect::<BTreeSet<_>>();
let unchecked = find!(
value: Id,
pattern!(facts, [{ (*scan) @ posture::unchecked: ?value }])
)
.collect::<BTreeSet<_>>();
let channel = one_optional(
find!(value: Id, pattern!(facts, [{ (*scan) @ posture::scan_channel: ?value }]))
.collect(),
"scan channel",
)?;
let scan_documents = find!(
value: Id,
pattern!(facts, [{ (*scan) @ posture::scan_document: ?value }])
)
.collect::<BTreeSet<_>>();
let scan_sightings = find!(
value: Id,
pattern!(facts, [{ (*scan) @ posture::scan_sighting: ?value }])
)
.collect::<BTreeSet<_>>();
let scan_omissions = find!(
value: Id,
pattern!(facts, [{ (*scan) @ posture::scan_omission: ?value }])
)
.collect::<BTreeSet<_>>();
if !scan_documents.is_subset(&documents)
|| !scan_sightings.is_subset(&sightings)
|| !scan_omissions.is_subset(&omissions)
{
bail!(
"scan {} references a missing or mistyped child",
fmt_id(*scan)
);
}
let checked_coverage = checked
.intersection(&coverage_modalities)
.copied()
.collect::<BTreeSet<_>>();
let covered = checked_coverage
.union(&unchecked)
.copied()
.collect::<BTreeSet<_>>();
if !checked.is_disjoint(&unchecked)
|| !checked.is_subset(&known_modalities)
|| !unchecked.is_subset(&coverage_modalities)
|| covered != coverage_modalities
{
bail!(
"scan {} does not partition every file-scan coverage modality into checked or unchecked",
fmt_id(*scan)
);
}
let actual_files = scan_documents.len() as u128;
let expected = entity! {
metadata::tag: KIND_SCAN,
metadata::created_at: created_at,
posture::target: target,
posture::file_count: file_count,
posture::checked*: checked,
posture::unchecked*: unchecked,
posture::scan_channel?: channel,
posture::scan_document*: scan_documents,
posture::scan_sighting*: scan_sightings,
posture::scan_omission*: scan_omissions,
}
.root()
.expect("scan identity has one root");
if expected != *scan {
bail!("scan {} is not intrinsic", fmt_id(*scan));
}
if inline_u256_to_u128(file_count)? != actual_files {
bail!(
"scan {} file_count does not match its documents",
fmt_id(*scan)
);
}
}
let mut document_paths = BTreeSet::new();
for document in &documents {
require_attributes(
facts,
*document,
[
metadata::tag.id(),
posture::path.id(),
posture::outcome.id(),
posture::detail.id(),
],
"document",
)?;
if entity_tags(facts, *document) != BTreeSet::from([KIND_DOCUMENT]) {
bail!("document {} has invalid tags", fmt_id(*document));
}
let owners = find!(
scan: Id,
pattern!(facts, [{ ?scan @ posture::scan_document: (*document) }])
)
.collect::<BTreeSet<_>>();
if owners.is_empty() || !owners.is_subset(&scans) {
bail!("document {} is not owned by a scan", fmt_id(*document));
}
let path = one_required(
find!(value: TextHandle, pattern!(facts, [{ (*document) @ posture::path: ?value }]))
.collect(),
"document path",
)?;
for scan in owners {
if !document_paths.insert((scan, path)) {
bail!("scan {} has multiple outcomes for one path", fmt_id(scan));
}
}
let outcome = one_required(
find!(value: Id, pattern!(facts, [{ (*document) @ posture::outcome: ?value }]))
.collect(),
"document outcome",
)?;
if ![OUTCOME_EXAMINED, DOC_UNSUPPORTED, OUTCOME_PARSE_FAILED].contains(&outcome) {
bail!("document {} has an unknown outcome", fmt_id(*document));
}
let detail = one_optional(
find!(value: TextHandle, pattern!(facts, [{ (*document) @ posture::detail: ?value }]))
.collect(),
"document detail",
)?;
if (outcome == OUTCOME_PARSE_FAILED) != detail.is_some() {
bail!(
"document {} has inconsistent parse-failure detail",
fmt_id(*document)
);
}
let expected = entity! {
metadata::tag: KIND_DOCUMENT,
posture::path: path,
posture::outcome: outcome,
posture::detail?: detail,
}
.root()
.expect("document identity has one root");
if expected != *document {
bail!("document {} is not intrinsic", fmt_id(*document));
}
}
for finding in &findings {
require_attributes(
facts,
*finding,
[
metadata::tag.id(),
posture::carrier_kind.id(),
posture::carrier.id(),
posture::locator.id(),
posture::span_start.id(),
posture::span_end.id(),
],
"finding",
)?;
let tags = entity_tags(facts, *finding);
let modalities = tags
.intersection(&known_modalities)
.copied()
.collect::<BTreeSet<_>>();
let modality = one_required(modalities, "finding modality")?;
if tags != BTreeSet::from([KIND_FINDING, modality]) {
bail!("finding {} has invalid tags", fmt_id(*finding));
}
let observers = find!(
sighting: Id,
pattern!(facts, [{ ?sighting @ posture::sighting_of: (*finding) }])
)
.collect::<BTreeSet<_>>();
if observers.is_empty() || !observers.is_subset(&sightings) {
bail!("finding {} was never sighted", fmt_id(*finding));
}
let carrier_kind = one_required(
find!(
value: Id,
pattern!(facts, [{ (*finding) @ posture::carrier_kind: ?value }])
)
.collect(),
"finding carrier kind",
)?;
if ![
CARRIER_GIT_BLOB,
CARRIER_CONTAINER_MEMBER,
CARRIER_GIT_COMMIT,
]
.contains(&carrier_kind)
{
bail!("finding {} has an unknown carrier kind", fmt_id(*finding));
}
let carrier = one_required(
find!(
value: TextHandle,
pattern!(facts, [{ (*finding) @ posture::carrier: ?value }])
)
.collect(),
"finding carrier",
)?;
let field = one_optional(
find!(
value: TextHandle,
pattern!(facts, [{ (*finding) @ posture::locator: ?value }])
)
.collect(),
"finding inner locator",
)?;
let span_start = one_optional(
find!(
value: Inline<inlineencodings::U256BE>,
pattern!(facts, [{ (*finding) @ posture::span_start: ?value }])
)
.collect(),
"finding span start",
)?;
let span_end = one_optional(
find!(
value: Inline<inlineencodings::U256BE>,
pattern!(facts, [{ (*finding) @ posture::span_end: ?value }])
)
.collect(),
"finding span end",
)?;
match (field, span_start, span_end) {
(Some(_), None, None) => {}
(None, Some(start), Some(end)) => {
if inline_u256_to_u128(start)? > inline_u256_to_u128(end)? {
bail!("finding {} has an inverted byte range", fmt_id(*finding));
}
}
_ => bail!(
"finding {} must carry exactly one of a byte range or a named coordinate",
fmt_id(*finding)
),
}
let expected = entity! {
metadata::tag: KIND_FINDING,
metadata::tag: modality,
posture::carrier_kind: carrier_kind,
posture::carrier: carrier,
posture::locator?: field,
posture::span_start?: span_start,
posture::span_end?: span_end,
}
.root()
.expect("finding identity has one root");
if expected != *finding {
bail!("finding {} is not content-located", fmt_id(*finding));
}
}
for sighting in &sightings {
require_attributes(
facts,
*sighting,
[
metadata::tag.id(),
posture::sighting_of.id(),
posture::document.id(),
posture::value.id(),
posture::evidence.id(),
posture::seen_in.id(),
],
"sighting",
)?;
if entity_tags(facts, *sighting) != BTreeSet::from([KIND_SIGHTING]) {
bail!("sighting {} has invalid tags", fmt_id(*sighting));
}
let owners = find!(
scan: Id,
pattern!(facts, [{ ?scan @ posture::scan_sighting: (*sighting) }])
)
.collect::<BTreeSet<_>>();
if owners.is_empty() || !owners.is_subset(&scans) {
bail!("sighting {} is not owned by a scan", fmt_id(*sighting));
}
let finding = one_required(
find!(
value: Id,
pattern!(facts, [{ (*sighting) @ posture::sighting_of: ?value }])
)
.collect(),
"sighting finding",
)?;
if !findings.contains(&finding) {
bail!("sighting {} names a missing finding", fmt_id(*sighting));
}
let modality = one_required(
entity_tags(facts, finding)
.intersection(&known_modalities)
.copied()
.collect(),
"sighted finding modality",
)?;
let document = one_required(
find!(
value: Id,
pattern!(facts, [{ (*sighting) @ posture::document: ?value }])
)
.collect(),
"sighting document",
)?;
if !documents.contains(&document) {
bail!(
"sighting {} references a missing document",
fmt_id(*sighting)
);
}
for scan in &owners {
if !exists!(pattern!(facts, [{ (*scan) @ posture::scan_document: (document) }])) {
bail!(
"sighting {} references a document outside scan {}",
fmt_id(*sighting),
fmt_id(*scan)
);
}
if !exists!(pattern!(facts, [{ (*scan) @ posture::checked: (modality) }])) {
bail!(
"sighting {} uses a modality scan {} did not mark checked",
fmt_id(*sighting),
fmt_id(*scan)
);
}
}
let document_outcome = one_required(
find!(
value: Id,
pattern!(facts, [{ (document) @ posture::outcome: ?value }])
)
.collect(),
"sighting document outcome",
)?;
if document_outcome != OUTCOME_EXAMINED {
bail!(
"sighting {} belongs to a document that was not examined",
fmt_id(*sighting)
);
}
let value = one_required(
find!(
value: TextHandle,
pattern!(facts, [{ (*sighting) @ posture::value: ?value }])
)
.collect(),
"sighting value",
)?;
let evidence = one_required(
find!(
value: TextHandle,
pattern!(facts, [{ (*sighting) @ posture::evidence: ?value }])
)
.collect(),
"sighting evidence",
)?;
let seen_in = one_optional(
find!(
value: TextHandle,
pattern!(facts, [{ (*sighting) @ posture::seen_in: ?value }])
)
.collect(),
"sighting commit",
)?;
let expected = entity! {
metadata::tag: KIND_SIGHTING,
posture::sighting_of: finding,
posture::document: document,
posture::value: value,
posture::evidence: evidence,
posture::seen_in?: seen_in,
}
.root()
.expect("sighting identity has one root");
if expected != *sighting {
bail!("sighting {} is not intrinsic", fmt_id(*sighting));
}
}
for omission in &omissions {
require_attributes(
facts,
*omission,
[metadata::tag.id(), posture::path.id(), posture::detail.id()],
"omission",
)?;
if entity_tags(facts, *omission) != BTreeSet::from([KIND_OMISSION]) {
bail!("omission {} has invalid tags", fmt_id(*omission));
}
let owners = find!(
scan: Id,
pattern!(facts, [{ ?scan @ posture::scan_omission: (*omission) }])
)
.collect::<BTreeSet<_>>();
if owners.is_empty() || !owners.is_subset(&scans) {
bail!("omission {} is not owned by a scan", fmt_id(*omission));
}
let path = one_required(
find!(value: TextHandle, pattern!(facts, [{ (*omission) @ posture::path: ?value }]))
.collect(),
"omission path",
)?;
let detail = one_required(
find!(value: TextHandle, pattern!(facts, [{ (*omission) @ posture::detail: ?value }]))
.collect(),
"omission detail",
)?;
let expected = entity! {
metadata::tag: KIND_OMISSION,
posture::path: path,
posture::detail: detail,
}
.root()
.expect("omission identity has one root");
if expected != *omission {
bail!("omission {} is not intrinsic", fmt_id(*omission));
}
}
Ok(())
}
#[derive(Debug)]
pub enum FileOutcome {
Examined,
Unsupported,
ParseFailed(String),
}
#[derive(Debug)]
pub struct ScannedFile {
pub path: PathBuf,
pub outcome: FileOutcome,
pub findings: Vec<Finding>,
}
fn unchecked_modalities() -> BTreeSet<Id> {
modality::ALL
.iter()
.map(|(id, _)| *id)
.filter(|id| !IMPLEMENTED.contains(id))
.collect()
}
fn build_scan_fragment(
target: &Path,
files: &[ScannedFile],
omissions: &[WalkOmission],
created_at: IntervalValue,
channel: Option<Id>,
checked: BTreeSet<Id>,
) -> (Fragment, Id) {
let mut fragment = Fragment::empty();
let target_handle: TextHandle = fragment.put(target.display().to_string());
let unchecked = modality::ALL
.iter()
.map(|(id, _)| *id)
.filter(|id| !checked.contains(id))
.collect::<BTreeSet<_>>();
let mut documents = BTreeSet::new();
let mut sightings = BTreeSet::new();
let mut omitted_paths = BTreeSet::new();
for file in files {
let path: TextHandle = fragment.put(file.path.display().to_string());
let (outcome, detail) = match &file.outcome {
FileOutcome::Examined => (OUTCOME_EXAMINED, None),
FileOutcome::Unsupported => (DOC_UNSUPPORTED, None),
FileOutcome::ParseFailed(error) => {
let detail: TextHandle = fragment.put(error.clone());
(OUTCOME_PARSE_FAILED, Some(detail))
}
};
let document = entity! {
metadata::tag: KIND_DOCUMENT,
posture::path: path,
posture::outcome: outcome,
posture::detail?: detail,
};
let document_id = document.root().expect("intrinsic document has one root");
documents.insert(document_id);
fragment += document;
for found in &file.findings {
let finding = finding_entity(&mut fragment, found.modality, &found.location);
sightings.insert(sighting_entity(&mut fragment, finding, document_id, found));
}
}
for omitted in omissions {
let path: TextHandle = fragment.put(omitted.path.display().to_string());
let detail: TextHandle = fragment.put(omitted.detail.clone());
let omission = entity! {
metadata::tag: KIND_OMISSION,
posture::path: path,
posture::detail: detail,
};
omitted_paths.insert(omission.root().expect("intrinsic omission has one root"));
fragment += omission;
}
let scan = entity! {
metadata::tag: KIND_SCAN,
metadata::created_at: created_at,
posture::target: target_handle,
posture::file_count: files.len() as u64,
posture::checked*: checked,
posture::unchecked*: unchecked,
posture::scan_channel?: channel,
posture::scan_document*: documents,
posture::scan_sighting*: sightings,
posture::scan_omission*: omitted_paths,
};
let scan_id = scan.root().expect("intrinsic scan has one Merkle root");
fragment += scan;
(fragment, scan_id)
}
fn cmd_scan(storage: PostureStorage<'_>, target: &Path, dry_run: bool) -> Result<ScanReport> {
let mut paths = Vec::new();
let mut omissions = Vec::new();
walk(target, &mut paths, &mut omissions);
paths.sort();
omissions.sort_by(|left, right| (&left.path, &left.detail).cmp(&(&right.path, &right.detail)));
let files = paths
.into_iter()
.map(|path| {
match std::fs::File::open(&path)
.with_context(|| format!("open {} for type detection", path.display()))
.and_then(|reader| inspect_reader(&path.to_string_lossy(), reader))
{
Ok(mut file) => {
file.path = path;
file
}
Err(error) => ScannedFile {
path,
outcome: FileOutcome::ParseFailed(error.to_string()),
findings: Vec::new(),
},
}
})
.collect();
finish_scan(
storage,
&target.to_string_lossy(),
files,
omissions,
dry_run,
)
}
fn finish_scan(
storage: PostureStorage<'_>,
target: &str,
files: Vec<ScannedFile>,
omissions: Vec<WalkOmission>,
dry_run: bool,
) -> Result<ScanReport> {
let checked: BTreeSet<Id> = IMPLEMENTED.iter().copied().collect();
let scan_id = if dry_run {
None
} else {
let (fragment, scan) = build_scan_fragment(
Path::new(target),
&files,
&omissions,
point_interval(now_epoch()?),
None,
checked.clone(),
);
storage.publish_scan(fragment, "posture complete scan")?;
Some(scan)
};
Ok(ScanReport {
scan_id,
target: target.to_owned(),
files,
omissions,
checked,
unchecked: unchecked_modalities(),
})
}
fn parse_scan_id(raw: Option<&str>) -> Result<Option<Id>> {
raw.map(|value| Id::from_hex(value.trim()).ok_or_else(|| anyhow!("invalid scan id '{value}'")))
.transpose()
}
fn all_scan_ids<P: TriblePattern>(space: &P) -> BTreeSet<Id> {
find!(
scan: Id,
pattern!(space, [{ ?scan @ metadata::tag: (&KIND_SCAN) }])
)
.collect()
}
fn select_scan<P: TriblePattern>(space: &P, requested: Option<&str>) -> Result<Option<Id>> {
if let Some(scan) = parse_scan_id(requested)? {
if !exists!(pattern!(space, [{ (scan) @ metadata::tag: (&KIND_SCAN) }])) {
bail!(
"scan {} is not present in the authorized scan collection",
fmt_id(scan)
);
}
return Ok(Some(scan));
}
let mut newest: Option<(i128, Id)> = None;
let mut tied = Vec::new();
for scan in all_scan_ids(space) {
let created_at = one_required(
find!(
value: IntervalValue,
pattern!(space, [{ (scan) @ metadata::created_at: ?value }])
)
.collect(),
"scan created_at",
)?;
let key = interval_key(created_at);
match newest {
None => {
newest = Some((key, scan));
tied = vec![scan];
}
Some((current, _)) if key > current => {
newest = Some((key, scan));
tied = vec![scan];
}
Some((current, _)) if key == current => tied.push(scan),
_ => {}
}
}
if tied.len() > 1 {
bail!(
"{} scans share the newest timestamp; pass an explicit scan id",
tied.len()
);
}
Ok(newest.map(|(_, scan)| scan))
}
const LEGACY_BENIGN_OUTCOME: &str = "benign";
#[derive(Default)]
struct FindingDecisionState {
benign: bool,
justified_benign: bool,
disputed: bool,
}
struct Settled {
ordinary: BTreeSet<Id>,
justified: BTreeSet<Id>,
disputed: BTreeSet<Id>,
bridges: BTreeMap<Id, BTreeSet<Id>>,
}
impl Settled {
fn hides(&self, modality: Id, finding: Id) -> bool {
self.hides_any(modality, std::iter::once(finding))
}
fn hides_any(&self, modality: Id, findings: impl IntoIterator<Item = Id>) -> bool {
let settled = if modality == modality::UNSAFE_ATTRIBUTE_ID {
&self.justified
} else {
&self.ordinary
};
let expanded = findings
.into_iter()
.flat_map(|finding| {
std::iter::once(finding).chain(
self.bridges
.get(&finding)
.into_iter()
.flat_map(|legacy| legacy.iter().copied()),
)
})
.collect::<BTreeSet<_>>();
!expanded.iter().any(|id| self.disputed.contains(id))
&& expanded.iter().any(|id| settled.contains(id))
}
}
fn findings_at<P: TriblePattern>(facts: &P, modality: Id, location: &Location) -> BTreeSet<Id> {
let mut probe = Fragment::empty();
let carrier: TextHandle = probe.put(location.carrier.address().to_owned());
let carrier_kind = location.carrier.kind();
match &location.inner {
Inner::Field(field) => {
let field: TextHandle = probe.put(field.clone());
find!(
finding: Id,
pattern!(facts, [{
?finding @
metadata::tag: (&KIND_FINDING),
metadata::tag: (modality),
posture::carrier_kind: (carrier_kind),
posture::carrier: (carrier),
posture::locator: (field)
}])
)
.collect()
}
Inner::Span { start, end } => find!(
finding: Id,
pattern!(facts, [{
?finding @
metadata::tag: (&KIND_FINDING),
metadata::tag: (modality),
posture::carrier_kind: (carrier_kind),
posture::carrier: (carrier),
posture::span_start: (*start),
posture::span_end: (*end)
}])
)
.collect(),
}
}
fn legacy_bridges<P: TriblePattern>(facts: &P) -> BTreeMap<Id, BTreeSet<Id>> {
let mut bridges = BTreeMap::<Id, BTreeSet<Id>>::new();
for (finding, legacy) in find!(
(finding: Id, legacy: Id),
pattern!(facts, [{
_?bridge @
metadata::tag: (&KIND_LEGACY_BRIDGE),
posture::sighting_of: ?finding,
posture::occurrence: ?legacy
}])
) {
bridges.entry(finding).or_default().insert(legacy);
}
bridges
}
fn settled_findings<P: TriblePattern>(
reader: &PileSnapshot,
facts: &P,
bridges: BTreeMap<Id, BTreeSet<Id>>,
) -> Result<Settled> {
let mut states = BTreeMap::<Id, FindingDecisionState>::new();
for decision in decide::decision_anchors(facts) {
let genesis = decide::genesis_for_decision(facts, decision)?
.ok_or_else(|| anyhow!("Decide decision {} has no genesis", fmt_id(decision)))?;
let Some(about) = genesis.about else {
continue;
};
let state = states.entry(about).or_default();
let snapshot = match decide::resolution(facts, decision) {
Resolution::Missing => continue,
Resolution::Unique(snapshot) => snapshot,
Resolution::Agreed(snapshots) => snapshots.into_iter().next().ok_or_else(|| {
anyhow!("Decide decision {} has empty agreement", fmt_id(decision))
})?,
Resolution::Forked(_) => {
state.disputed = true;
continue;
}
Resolution::Invalid(error) => {
bail!("Decide decision {} is invalid: {error}", fmt_id(decision));
}
};
let benign = match snapshot.result {
Some(result) => result == decide::RESULT_BENIGN,
None => decide::read_text(reader, snapshot.outcome)? == LEGACY_BENIGN_OUTCOME,
};
if benign {
state.benign = true;
if let Some(context) = genesis.context {
if !decide::read_text(reader, context)?.trim().is_empty() {
state.justified_benign = true;
}
}
} else {
state.disputed = true;
}
}
let ordinary = states
.iter()
.filter_map(|(finding, state)| (state.benign && !state.disputed).then_some(*finding))
.collect();
let disputed = states
.iter()
.filter_map(|(finding, state)| state.disputed.then_some(*finding))
.collect();
let justified = states
.into_iter()
.filter_map(|(finding, state)| {
(state.justified_benign && !state.disputed).then_some(finding)
})
.collect();
Ok(Settled {
ordinary,
justified,
disputed,
bridges,
})
}
#[cfg(test)]
fn benign_occurrences<P: TriblePattern>(reader: &PileSnapshot, facts: &P) -> Result<BTreeSet<Id>> {
Ok(settled_findings(reader, facts, BTreeMap::new())?.ordinary)
}
fn cmd_list(
storage: PostureStorage<'_>,
scan: Option<String>,
examples: usize,
all: bool,
) -> Result<FindingList> {
let (view, decisions) = storage.scan_and_decide_views()?;
let want = parse_scan_id(scan.as_deref())?;
if let Some(scan) = want {
if !all_scan_ids(&view.facts).contains(&scan) {
bail!(
"scan {} is not present in the authorized scan collection",
fmt_id(scan)
);
}
}
let settled = settled_findings(
&decisions.reader,
&decisions.facts,
legacy_bridges(&view.facts),
)?;
let mut all_rows = find!(
(scan: Id, finding: Id, evidence: TextHandle, value: TextHandle),
pattern!(&view.facts, [{
?scan @ posture::scan_sighting: _?sighting,
}, {
_?sighting @
metadata::tag: (&KIND_SIGHTING),
posture::sighting_of: ?finding,
posture::evidence: ?evidence,
posture::value: ?value
}])
)
.map(|(scan, finding, evidence, value)| (scan, finding, finding, evidence, value))
.collect::<Vec<_>>();
all_rows.extend(
find!(
(scan: Id, finding: Id, occurrence: Id, locator: TextHandle, value: TextHandle),
pattern!(&view.facts, [{
?scan @ posture::scan_finding: ?finding,
}, {
?finding @
metadata::tag: (&KIND_FINDING),
posture::occurrence: ?occurrence,
posture::locator: ?locator,
posture::value: ?value
}])
)
.map(|(scan, finding, occurrence, locator, value)| {
(scan, occurrence, finding, locator, value)
}),
);
let all_rows = all_rows
.into_iter()
.filter(|(scan, _, _, _, _)| want.is_none_or(|wanted| *scan == wanted))
.map(|(scan, id, tagged, evidence, value)| {
let modality = find!(
tag: Id,
pattern!(&view.facts, [{ (tagged) @ metadata::tag: ?tag }])
)
.find(|tag| modality::is_known(*tag))
.expect("a finding carries one known modality");
(scan, id, evidence, value, modality)
})
.collect::<Vec<_>>();
let hidden = all_rows
.iter()
.filter(|(_, finding, _, _, modality)| settled.hides(*modality, *finding))
.count();
let rows = all_rows
.into_iter()
.filter(|(_, finding, _, _, modality)| all || !settled.hides(*modality, *finding))
.collect::<Vec<_>>();
let mut groups: BTreeMap<&str, Vec<(Id, TextHandle, TextHandle)>> = BTreeMap::new();
for (_, finding, evidence, value, modality) in &rows {
groups
.entry(modality::name(*modality))
.or_default()
.push((*finding, *evidence, *value));
}
let groups = groups
.into_iter()
.map(|(name, items)| {
let selected = items
.iter()
.take(examples)
.map(|(id, evidence, value)| {
Ok(FindingExample {
id: *id,
evidence: read_text(&view.reader, *evidence, "finding evidence")?,
value: read_text(&view.reader, *value, "finding value")?,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(FindingGroup {
name,
count: items.len(),
examples: selected,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(FindingList {
scan: want,
hidden,
include_resolved: all,
groups,
})
}
fn cmd_coverage(storage: PostureStorage<'_>, scan: Option<String>) -> Result<Option<Coverage>> {
let view = storage.scan_view()?;
let Some(scan) = select_scan(&view.facts, scan.as_deref())? else {
return Ok(None);
};
let target = one_required(
find!(value: TextHandle, pattern!(&view.facts, [{
scan @ posture::target: ?value
}]))
.collect(),
"scan target",
)?;
let checked =
find!(modality: Id, pattern!(&view.facts, [{ scan @ posture::checked: ?modality }]))
.collect();
let unchecked =
find!(modality: Id, pattern!(&view.facts, [{ scan @ posture::unchecked: ?modality }]))
.collect();
let mut outcomes = BTreeMap::<Id, usize>::new();
for outcome in find!(outcome: Id, pattern!(&view.facts, [{
scan @ posture::scan_document: _?document
}, {
_?document @ metadata::tag: KIND_DOCUMENT, posture::outcome: ?outcome
}])) {
*outcomes.entry(outcome).or_default() += 1;
}
let omissions = find!((path: TextHandle, detail: TextHandle), pattern!(&view.facts, [{
scan @ posture::scan_omission: _?omission
}, {
_?omission @ metadata::tag: KIND_OMISSION, posture::path: ?path, posture::detail: ?detail
}]))
.map(|(path, detail)| {
Ok(WalkOmission {
path: PathBuf::from(read_text(&view.reader, path, "omission path")?),
detail: read_text(&view.reader, detail, "omission detail")?,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(Some(Coverage {
scan,
target: read_text(&view.reader, target, "scan target")?,
checked,
unchecked,
outcomes,
omissions,
}))
}
fn cmd_scans(storage: PostureStorage<'_>) -> Result<Vec<ScanRecord>> {
let view = storage.scan_view()?;
let mut scans = Vec::new();
for scan in all_scan_ids(&view.facts) {
let target = one_required(
find!(
value: TextHandle,
pattern!(&view.facts, [{ (scan) @ posture::target: ?value }])
)
.collect(),
"scan target",
)?;
let created_at = one_required(
find!(
value: IntervalValue,
pattern!(&view.facts, [{ (scan) @ metadata::created_at: ?value }])
)
.collect(),
"scan created_at",
)?;
let findings = find!(
sighting: Id,
pattern!(&view.facts, [{
(scan) @ posture::scan_sighting: ?sighting,
}, {
?sighting @ metadata::tag: (&KIND_SIGHTING)
}])
)
.count()
+ find!(
finding: Id,
pattern!(&view.facts, [{
(scan) @ posture::scan_finding: ?finding,
}, {
?finding @ metadata::tag: (&KIND_FINDING)
}])
)
.count();
scans.push((
interval_key(created_at),
scan,
findings,
read_text(&view.reader, target, "scan target")?,
));
}
scans.sort_by(|left, right| right.cmp(left));
Ok(scans
.into_iter()
.map(|(created_at, id, findings, target)| ScanRecord {
id,
created_at,
findings,
target,
})
.collect())
}
fn canonical_channel(raw: &str) -> Result<String> {
let channel = raw.trim().to_lowercase();
if channel.is_empty() {
bail!("channel name cannot be empty");
}
Ok(channel)
}
fn canonical_term(raw: &str) -> Result<String> {
let term = raw.trim().to_lowercase();
if term.is_empty() {
bail!("protected term cannot be empty");
}
Ok(term)
}
#[cfg(any(feature = "local-embed", test))]
fn canonical_exemplar(raw: &str) -> String {
raw.replace("\r\n", "\n")
.replace('\r', "\n")
.trim()
.to_owned()
}
fn append_channel(fragment: &mut Fragment, name: &str) -> Id {
let name: TextHandle = fragment.put(name.to_owned());
let channel = entity! {
metadata::tag: KIND_CHANNEL,
posture::channel_name: name,
};
let id = channel.root().expect("intrinsic channel has one root");
*fragment += channel;
id
}
fn channel_by_name<P: TriblePattern>(
reader: &PileSnapshot,
space: &P,
raw: &str,
) -> Result<Option<Id>> {
let wanted = canonical_channel(raw)?;
let mut matches = Vec::new();
for (channel, name) in find!(
(channel: Id, name: TextHandle),
pattern!(space, [{
?channel @ metadata::tag: (&KIND_CHANNEL), posture::channel_name: ?name
}])
) {
if read_text(reader, name, "channel name")? == wanted {
matches.push(channel);
}
}
matches.sort_unstable();
matches.dedup();
Ok(matches.into_iter().min())
}
#[derive(Debug)]
enum PolicyHead {
Missing,
Unique { revision: Id, members: BTreeSet<Id> },
Forked(Vec<Id>),
}
fn resolve_policy_head<P: TriblePattern>(space: &P, channel: Id) -> Result<PolicyHead> {
let revisions = find!(
revision: Id,
pattern!(space, [{
?revision @
metadata::tag: (&KIND_POLICY_REVISION),
posture::in_channel: (channel)
}])
)
.collect::<BTreeSet<_>>();
if revisions.is_empty() {
return Ok(PolicyHead::Missing);
}
let superseded = revisions
.iter()
.flat_map(|revision| {
find!(
predecessor: Id,
pattern!(space, [{ *revision @ metadata::supersedes: ?predecessor }])
)
})
.collect::<BTreeSet<_>>();
let heads = revisions
.difference(&superseded)
.copied()
.collect::<Vec<_>>();
match heads.as_slice() {
[revision] => {
let members = find!(
member: Id,
pattern!(space, [{ (*revision) @ posture::policy_member: ?member }])
)
.collect();
Ok(PolicyHead::Unique {
revision: *revision,
members,
})
}
_ => Ok(PolicyHead::Forked(heads)),
}
}
fn append_policy_revision(
fragment: &mut Fragment,
channel: Id,
members: &BTreeSet<Id>,
predecessors: &BTreeSet<Id>,
) -> Id {
let revision = entity! {
metadata::tag: KIND_POLICY_REVISION,
posture::in_channel: channel,
posture::policy_member*: members.clone(),
metadata::supersedes*: predecessors.clone(),
};
let id = revision
.root()
.expect("intrinsic policy revision has one root");
*fragment += revision;
id
}
fn policy_members<P: TriblePattern>(space: &P, channel: Id) -> Result<(Option<Id>, BTreeSet<Id>)> {
match resolve_policy_head(space, channel)? {
PolicyHead::Missing => Ok((None, BTreeSet::new())),
PolicyHead::Unique { revision, members } => Ok((Some(revision), members)),
PolicyHead::Forked(heads) => bail!(
"channel {} is FORKED across {} policy heads ({}); reconcile explicitly before use",
fmt_id(channel),
heads.len(),
heads
.iter()
.map(|head| fmt_id(*head))
.collect::<Vec<_>>()
.join(", ")
),
}
}
#[cfg(any(feature = "local-embed", test))]
fn take_exemplars_with_body(
reader: &PileSnapshot,
space: &impl TriblePattern,
members: &mut BTreeSet<Id>,
body: &str,
) -> Result<BTreeSet<Id>> {
let mut replaced = BTreeSet::new();
for member in members.iter().copied() {
if !exists!(pattern!(space, [{
(member) @ metadata::tag: (&KIND_EXEMPLAR)
}])) {
continue;
}
let handle = one_required(
find!(
value: TextHandle,
pattern!(space, [{ (member) @ posture::term: ?value }])
)
.collect(),
"exemplar text",
)?;
if read_text(reader, handle, "exemplar text")? == body {
replaced.insert(member);
}
}
members.retain(|member| !replaced.contains(member));
Ok(replaced)
}
fn channel_terms(
reader: &PileSnapshot,
space: &impl TriblePattern,
channel: Id,
) -> Result<Vec<(String, String)>> {
let (_, members) = policy_members(space, channel)?;
let mut terms = Vec::new();
for member in members {
if !exists!(pattern!(space, [{ (member) @ metadata::tag: (&KIND_TERM) }])) {
continue;
}
let term = one_required(
find!(
value: TextHandle,
pattern!(space, [{ (member) @ posture::term: ?value }])
)
.collect(),
"term text",
)?;
let why = one_optional(
find!(
value: TextHandle,
pattern!(space, [{ (member) @ posture::why: ?value }])
)
.collect(),
"term rationale",
)?;
terms.push((
read_text(reader, term, "term text")?,
why.map(|handle| read_text(reader, handle, "term rationale"))
.transpose()?
.unwrap_or_default(),
));
}
terms.sort();
Ok(terms)
}
fn cmd_vocab_add(
storage: PostureStorage<'_>,
term: &str,
channel: &str,
why: Option<&str>,
) -> Result<PolicyReceipt> {
let channel_name = canonical_channel(channel)?;
let term_text = canonical_term(term)?;
let why_text = why.map(str::trim).filter(|text| !text.is_empty());
let view = storage.policy_view()?;
let mut fragment = Fragment::empty();
let channel_id = append_channel(&mut fragment, &channel_name);
if let Some(existing) = channel_by_name(&view.reader, &view.facts, &channel_name)? {
if existing != channel_id {
bail!("canonical channel identity disagrees with stored channel");
}
}
let (head, mut members) = policy_members(&view.facts, channel_id)?;
let mut replaced = BTreeSet::new();
for member in &members {
if !exists!(pattern!(&view.facts, [{ (*member) @ metadata::tag: (&KIND_TERM) }])) {
continue;
}
let handle = one_required(
find!(
value: TextHandle,
pattern!(&view.facts, [{ (*member) @ posture::term: ?value }])
)
.collect(),
"term text",
)?;
if read_text(&view.reader, handle, "term text")? == term_text {
replaced.insert(*member);
}
}
for member in &replaced {
members.remove(member);
}
let term_handle: TextHandle = fragment.put(term_text.clone());
let why_handle: Option<TextHandle> = why_text.map(|text| fragment.put(text.to_owned()));
let term = entity! {
metadata::tag: KIND_TERM,
posture::in_channel: channel_id,
posture::term: term_handle,
posture::role: EXEMPLAR_PROTECTED,
posture::why?: why_handle,
};
let term_id = term.root().expect("intrinsic term has one root");
fragment += term;
members.insert(term_id);
if replaced == BTreeSet::from([term_id]) {
return Ok(PolicyReceipt {
channel_id,
channel: channel_name,
member: term_id,
revision: None,
published: false,
kind: PolicyMemberKind::Term,
text: term_text,
});
}
let predecessors = head.into_iter().collect();
let revision = append_policy_revision(&mut fragment, channel_id, &members, &predecessors);
storage.publish_policy(fragment, "posture policy term")?;
Ok(PolicyReceipt {
channel_id,
channel: channel_name,
member: term_id,
revision: Some(revision),
published: true,
kind: PolicyMemberKind::Term,
text: term_text,
})
}
fn cmd_vocab_list(
storage: PostureStorage<'_>,
channel: Option<&str>,
) -> Result<Vec<VocabularyChannel>> {
let view = storage.policy_view()?;
let wanted = channel.map(canonical_channel).transpose()?;
let mut channels = Vec::new();
for (id, handle) in find!(
(channel: Id, name: TextHandle),
pattern!(&view.facts, [{
?channel @ metadata::tag: (&KIND_CHANNEL), posture::channel_name: ?name
}])
) {
let expected = entity! {
metadata::tag: KIND_CHANNEL,
posture::channel_name: handle,
}
.root()
.expect("canonical channel has one root");
if id != expected {
continue;
}
channels.push((read_text(&view.reader, handle, "channel name")?, id));
}
channels.sort();
channels.dedup();
channels
.into_iter()
.filter(|(name, _)| wanted.as_ref().is_none_or(|wanted| wanted == name))
.map(|(name, id)| {
let state = match resolve_policy_head(&view.facts, id)? {
PolicyHead::Missing => VocabularyState::Missing,
PolicyHead::Forked(heads) => VocabularyState::Forked(heads),
PolicyHead::Unique { revision, .. } => VocabularyState::Ready {
revision,
terms: channel_terms(&view.reader, &view.facts, id)?,
},
};
Ok(VocabularyChannel { id, name, state })
})
.collect()
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum UnsafeAttributeChange {
Added,
Removed,
}
impl UnsafeAttributeChange {
fn label(self) -> &'static str {
match self {
Self::Added => "added",
Self::Removed => "removed",
}
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct GitHit {
pub location: Location,
pub evidence: String,
pub seen_in: String,
pub display: String,
pub claim: Option<(UnsafeAttributeChange, String)>,
}
fn canonical_git_root(repo_path: &Path) -> Result<PathBuf> {
let root = git_required(repo_path, &["rev-parse", "--show-toplevel"])?;
let root = PathBuf::from(root.trim());
std::fs::canonicalize(&root)
.with_context(|| format!("canonicalize git repository root {}", root.display()))
}
#[derive(Debug)]
struct GitAudit {
repo_root: PathBuf,
hits: BTreeMap<String, Vec<GitHit>>,
unsafe_attribute_hits: Vec<GitHit>,
commits: usize,
added_lines: usize,
removed_lines: usize,
}
const UNSAFE_ATTRIBUTE_FINDING: &str =
"literal-pinned attribute identity (`unsafe as`) change requires justification";
fn abbreviated_object_id(object_id: &str) -> &str {
&object_id[..object_id.len().min(8)]
}
fn git_hit(
kind: &str,
location: Location,
sha: &str,
position: impl std::fmt::Display,
text: &str,
) -> GitHit {
let position = position.to_string();
GitHit {
location,
evidence: format!("{kind} {sha}:{position} {}", text.trim()),
seen_in: sha.to_owned(),
display: format!(
"{kind} {}:{position} {}",
abbreviated_object_id(sha),
text.trim()
),
claim: None,
}
}
fn unsafe_attribute_hit(
object_id: &str,
position: &str,
source_path: &str,
change: UnsafeAttributeChange,
declaration: &UnsafeAttributeDeclaration,
) -> GitHit {
let carrier = Carrier::member(declaration.text.as_bytes());
let claim = format!(
"{}\u{0}{source_path}#{}",
carrier.address(),
declaration.same_text_ordinal
);
let coordinate = format!(
"{} {source_path}#{}",
change.label(),
declaration.same_text_ordinal
);
GitHit {
location: Location::field(carrier, coordinate),
evidence: format!(
"rust-attribute-{} {source_path}#{} {}",
change.label(),
declaration.same_text_ordinal,
declaration.text
),
seen_in: object_id.to_owned(),
display: format!(
"unsafe-attribute-{} {}:{position} {}",
change.label(),
abbreviated_object_id(object_id),
declaration.text.trim()
),
claim: Some((change, claim)),
}
}
#[derive(Debug, Eq, PartialEq)]
struct UnsafeAttributeDeclaration {
start_line: u64,
end_line: u64,
text: String,
same_text_ordinal: usize,
}
fn unsafe_attribute_declarations(source: &str) -> Vec<UnsafeAttributeDeclaration> {
let comments = r"(?:(?://[^\n]*\n)|(?:/\*.*?\*/)|\s)*";
let pattern = format!(r#"(?ms)^[ \t]*"[0-9A-Fa-f]{{32}}"{comments}unsafe{comments}as\b"#);
let declaration_start =
Regex::new(&pattern).expect("unsafe attribute declaration regex is valid");
let mut declarations = Vec::new();
let mut text_counts = BTreeMap::<String, usize>::new();
for matched in declaration_start.find_iter(source) {
let Some(end) = rust_declaration_end(source, matched.end()) else {
continue;
};
let matched_text = &source[matched.start()..end];
let text = matched_text
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
let same_text_ordinal = *text_counts
.entry(text.clone())
.and_modify(|count| *count += 1)
.or_insert(1);
declarations.push(UnsafeAttributeDeclaration {
start_line: source[..matched.start()]
.bytes()
.filter(|byte| *byte == b'\n')
.count() as u64
+ 1,
end_line: source[..end].bytes().filter(|byte| *byte == b'\n').count() as u64 + 1,
text,
same_text_ordinal,
});
}
declarations
}
fn rust_declaration_end(source: &str, start: usize) -> Option<usize> {
let bytes = source.as_bytes();
let mut index = start;
let mut block_depth = 0usize;
let mut line_comment = false;
let mut string = false;
let mut character = false;
let mut escaped = false;
while index < bytes.len() {
let byte = bytes[index];
let next = bytes.get(index + 1).copied();
if line_comment {
if byte == b'\n' {
line_comment = false;
}
index += 1;
continue;
}
if block_depth > 0 {
if byte == b'/' && next == Some(b'*') {
block_depth += 1;
index += 2;
} else if byte == b'*' && next == Some(b'/') {
block_depth -= 1;
index += 2;
} else {
index += 1;
}
continue;
}
if string || character {
if escaped {
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if (string && byte == b'"') || (character && byte == b'\'') {
string = false;
character = false;
}
index += 1;
continue;
}
match (byte, next) {
(b'/', Some(b'/')) => {
line_comment = true;
index += 2;
}
(b'/', Some(b'*')) => {
block_depth = 1;
index += 2;
}
(b'"', _) => {
string = true;
index += 1;
}
(b'\'', _) if bytes[index + 1..bytes.len().min(index + 8)].contains(&b'\'') => {
character = true;
index += 1;
}
(b';', _) => return Some(index + 1),
_ => index += 1,
}
}
None
}
fn hunk_start(line: &str, prefix: char) -> Option<u64> {
line.split_whitespace().find_map(|field| {
field
.strip_prefix(prefix)
.and_then(|range| range.split(',').next())
.and_then(|start| start.parse::<u64>().ok())
})
}
fn append_changed_unsafe_attributes(
repo_root: &Path,
display_commit: &str,
treeish: &str,
change: UnsafeAttributeChange,
changed_lines: BTreeMap<String, BTreeSet<u64>>,
hits: &mut Vec<GitHit>,
) -> Result<()> {
for (patch_path, changed_lines) in changed_lines {
if patch_path == "/dev/null" {
continue;
}
let source_path = patch_path
.strip_prefix("a/")
.or_else(|| patch_path.strip_prefix("b/"))
.unwrap_or(&patch_path);
let object = format!("{treeish}:{source_path}");
let source = git_required(repo_root, &["show", &object])?;
for declaration in unsafe_attribute_declarations(&source) {
if !(declaration.start_line..=declaration.end_line)
.any(|line| changed_lines.contains(&line))
{
continue;
}
let position = if declaration.start_line == declaration.end_line {
format!("{source_path}:{}", declaration.start_line)
} else {
format!(
"{source_path}:{}-{}",
declaration.start_line, declaration.end_line
)
};
hits.push(unsafe_attribute_hit(
display_commit,
&position,
source_path,
change,
&declaration,
));
}
}
Ok(())
}
fn unsafe_attribute_claims(hits: &[GitHit], direction: UnsafeAttributeChange) -> BTreeSet<String> {
hits.iter()
.filter_map(|hit| hit.claim.as_ref())
.filter(|(change, _)| *change == direction)
.map(|(_, claim)| claim.clone())
.collect()
}
fn cancel_unchanged_unsafe_attributes(hits: &mut Vec<GitHit>) {
let added = unsafe_attribute_claims(hits, UnsafeAttributeChange::Added);
let removed = unsafe_attribute_claims(hits, UnsafeAttributeChange::Removed);
let unchanged = added
.intersection(&removed)
.cloned()
.collect::<BTreeSet<_>>();
hits.retain(|hit| {
hit.claim
.as_ref()
.is_none_or(|(_, claim)| !unchanged.contains(claim))
});
}
fn collect_parent_unsafe_hits(
repo_root: &Path,
sha: &str,
parent: Option<&str>,
unsafe_attribute_hits: &mut Vec<GitHit>,
) -> Result<usize> {
let patch = match parent {
Some(parent) => git_required(
repo_root,
&[
"diff",
"--no-color",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--diff-algorithm=myers",
"--unified=0",
parent,
sha,
],
)?,
None => git_required(
repo_root,
&[
"show",
"--format=",
"--no-color",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--diff-algorithm=myers",
"--unified=0",
sha,
],
)?,
};
let mut added_rust_lines = BTreeMap::<String, BTreeSet<u64>>::new();
let mut removed_rust_lines = BTreeMap::<String, BTreeSet<u64>>::new();
let mut old_patch_path = "?";
let mut new_patch_path = "?";
let mut next_old_line = None;
let mut next_new_line = None;
let mut n_removed = 0usize;
for line in patch.lines() {
if line.starts_with("diff --git ") {
old_patch_path = "?";
new_patch_path = "?";
next_old_line = None;
next_new_line = None;
continue;
}
if let Some(path) = line.strip_prefix("--- ") {
old_patch_path = path;
next_old_line = None;
continue;
}
if let Some(path) = line.strip_prefix("+++ ") {
new_patch_path = path;
next_new_line = None;
continue;
}
if line.starts_with("@@") {
next_old_line = hunk_start(line, '-');
next_new_line = hunk_start(line, '+');
continue;
}
if line.starts_with('-') && !line.starts_with("---") {
n_removed += 1;
if old_patch_path.ends_with(".rs") {
if let Some(source_line) = next_old_line {
removed_rust_lines
.entry(old_patch_path.to_owned())
.or_default()
.insert(source_line);
}
}
next_old_line = next_old_line.map(|line| line + 1);
continue;
}
if !line.starts_with('+') || line.starts_with("+++") {
if !line.starts_with('\\') {
next_old_line = next_old_line.map(|line| line + 1);
next_new_line = next_new_line.map(|line| line + 1);
}
continue;
}
let added_line = next_new_line;
next_new_line = next_new_line.map(|line| line + 1);
if new_patch_path.ends_with(".rs") {
if let Some(line) = added_line {
added_rust_lines
.entry(new_patch_path.to_owned())
.or_default()
.insert(line);
}
}
}
let mut edge_unsafe_hits = Vec::new();
append_changed_unsafe_attributes(
repo_root,
sha,
sha,
UnsafeAttributeChange::Added,
added_rust_lines,
&mut edge_unsafe_hits,
)?;
if !removed_rust_lines.is_empty() {
let parent =
parent.ok_or_else(|| anyhow!("root commit {sha} unexpectedly removes source lines"))?;
append_changed_unsafe_attributes(
repo_root,
sha,
parent,
UnsafeAttributeChange::Removed,
removed_rust_lines,
&mut edge_unsafe_hits,
)?;
}
cancel_unchanged_unsafe_attributes(&mut edge_unsafe_hits);
unsafe_attribute_hits.extend(edge_unsafe_hits);
Ok(n_removed)
}
fn collect_commit_patch_term_hits(
repo_root: &Path,
sha: &str,
terms: &[(String, String)],
objects: &mut GitObjects,
hits: &mut BTreeMap<String, Vec<GitHit>>,
) -> Result<usize> {
let patch = git_required(
repo_root,
&[
"show",
"--format=",
"--no-color",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--diff-algorithm=myers",
"--unified=0",
sha,
],
)?;
let mut patch_path = "?";
let mut next_new_line = None;
let mut n_added = 0usize;
for (diff_line_index, line) in patch.lines().enumerate() {
if let Some(path) = line.strip_prefix("+++ ") {
patch_path = path;
next_new_line = None;
continue;
}
if line.starts_with("@@") {
next_new_line = hunk_start(line, '+');
continue;
}
if !line.starts_with('+') || line.starts_with("+++") {
if next_new_line.is_some() && !line.starts_with('-') && !line.starts_with('\\') {
next_new_line = next_new_line.map(|line| line + 1);
}
continue;
}
n_added += 1;
let added_line = next_new_line;
let position = match added_line {
Some(new_line) => format!("{patch_path}:{new_line}:diff-{}", diff_line_index + 1),
None => format!("{patch_path}:diff-{}", diff_line_index + 1),
};
next_new_line = next_new_line.map(|line| line + 1);
let source_path = patch_path
.strip_prefix("a/")
.or_else(|| patch_path.strip_prefix("b/"))
.unwrap_or(patch_path);
let lower = line.to_lowercase();
for (term, _) in terms {
if !lower.contains(&term.to_lowercase()) {
continue;
}
let location = match added_line {
Some(new_line) => objects.locate(repo_root, sha, source_path, new_line, term)?,
None => Location::field(Carrier::Commit(sha.to_owned()), position.clone()),
};
hits.entry(term.clone())
.or_default()
.push(git_hit("patch", location, sha, &position, line));
}
}
Ok(n_added)
}
fn collect_hits(
repo_path: &Path,
revisions: &[String],
terms: &[(String, String)],
) -> Result<GitAudit> {
let repo_root = canonical_git_root(repo_path)?;
let mut objects = GitObjects::default();
let mut hits: BTreeMap<String, Vec<GitHit>> = BTreeMap::new();
let mut unsafe_attribute_hits = Vec::new();
let mut n_commits = 0usize;
let mut n_added = 0usize;
let mut n_removed = 0usize;
let log = git_required(
&repo_root,
&git_log_args(revisions, "--format=%H%x1e%B%x1f"),
)?;
for rec in log.split('\u{1f}') {
let rec = rec.trim_start_matches('\n');
if rec.is_empty() {
continue;
}
n_commits += 1;
let (sha, body) = rec.split_once('\u{1e}').unwrap_or(("?", rec));
let sha = sha.trim();
let mut offset = 0usize;
for (line_index, raw) in body.split_inclusive('\n').enumerate() {
let line = raw.trim_end_matches(['\n', '\r']);
let lower = line.to_lowercase();
for (t, _) in terms {
if !lower.contains(&t.to_lowercase()) {
continue;
}
let location = commit_message_location(sha, offset, line, line_index, t);
hits.entry(t.clone()).or_default().push(git_hit(
"message",
location,
sha,
line_index + 1,
line,
));
}
offset += raw.len();
}
}
let shas: Vec<String> = git_required(&repo_root, &git_log_args(revisions, "--format=%H"))?
.lines()
.map(str::to_string)
.collect();
for sha in &shas {
for (path_index, path) in git_required(
&repo_root,
&["show", "--format=", "--name-only", "--no-renames", sha],
)?
.lines()
.filter(|path| !path.is_empty())
.enumerate()
{
let lower = path.to_lowercase();
let carrier = match objects.blob_at(&repo_root, sha, path)? {
Some(oid) => Carrier::GitBlob(oid),
None => Carrier::Commit(sha.to_owned()),
};
for (t, _) in terms {
if lower.contains(&t.to_lowercase()) {
hits.entry(t.clone()).or_default().push(git_hit(
"path",
Location::field(carrier.clone(), path.to_owned()),
sha,
format!("{}:{path}", path_index + 1),
path,
));
}
}
}
let lineage = git_required(&repo_root, &["rev-list", "--parents", "-n", "1", sha])?;
let parents = lineage.split_whitespace().skip(1).collect::<Vec<_>>();
if parents.is_empty() {
let removed =
collect_parent_unsafe_hits(&repo_root, sha, None, &mut unsafe_attribute_hits)?;
n_removed += removed;
} else {
for parent in parents {
let removed = collect_parent_unsafe_hits(
&repo_root,
sha,
Some(parent),
&mut unsafe_attribute_hits,
)?;
n_removed += removed;
}
}
n_added += collect_commit_patch_term_hits(&repo_root, sha, terms, &mut objects, &mut hits)?;
}
for term_hits in hits.values_mut() {
term_hits.sort();
term_hits.dedup();
}
unsafe_attribute_hits.sort();
unsafe_attribute_hits.dedup();
Ok(GitAudit {
repo_root,
hits,
unsafe_attribute_hits,
commits: n_commits,
added_lines: n_added,
removed_lines: n_removed,
})
}
fn git_log_args<'a>(revisions: &'a [String], format: &'a str) -> Vec<&'a str> {
let mut args = vec!["log"];
args.extend(revisions.iter().map(String::as_str));
args.push(format);
args
}
fn git_required(repo_path: &Path, args: &[&str]) -> Result<String> {
let output = std::process::Command::new("git")
.env("LC_ALL", "C")
.arg("-C")
.arg(repo_path)
.args(args)
.output()
.with_context(|| format!("run git -C {} {}", repo_path.display(), args.join(" ")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
bail!(
"git -C {} {} failed{}",
repo_path.display(),
args.join(" "),
if stderr.is_empty() {
String::new()
} else {
format!(": {stderr}")
}
);
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(test)]
fn load_channel_terms(
storage: PostureStorage<'_>,
channel: &str,
) -> Result<Option<(Id, Vec<(String, String)>)>> {
let view = storage.policy_view()?;
channel_terms_from_view(&view, channel)
}
fn channel_terms_from_view(
view: &CollectionView,
channel: &str,
) -> Result<Option<(Id, Vec<(String, String)>)>> {
let Some(channel) = channel_by_name(&view.reader, &view.facts, channel)? else {
return Ok(None);
};
Ok(Some((
channel,
channel_terms(&view.reader, &view.facts, channel)?,
)))
}
#[cfg(test)]
fn load_terms(storage: PostureStorage<'_>, channel: &str) -> Result<Vec<(String, String)>> {
Ok(load_channel_terms(storage, channel)?
.map(|(_, terms)| terms)
.unwrap_or_default())
}
fn git_audit_must_fail(
lexical_checked: bool,
protected_hits: usize,
unsafe_attribute_hits: usize,
) -> bool {
!lexical_checked || protected_hits > 0 || unsafe_attribute_hits > 0
}
fn cmd_git(
storage: PostureStorage<'_>,
revisions: &[String],
channel: &str,
repo_path: &Path,
) -> Result<GitReport> {
if let Some(bad) = revisions
.iter()
.find(|rev| rev.starts_with("--channel") || rev.starts_with("--repo"))
{
bail!(
"{bad} was read as a revision, not an option; put --channel/--repo before the revisions"
);
}
let range = revisions.join(" ");
let (policy_view, scan_view, decisions) = storage.policy_scan_and_decide_views()?;
let policy = channel_terms_from_view(&policy_view, channel)?;
let (channel_id, terms) = match policy {
Some((channel_id, terms)) => (Some(channel_id), terms),
None => (None, Vec::new()),
};
let lexical_checked = !terms.is_empty();
let GitAudit {
repo_root,
hits,
unsafe_attribute_hits,
commits: n_commits,
added_lines: n_added,
removed_lines: n_removed,
} = collect_hits(repo_path, revisions, &terms)?;
let settled = settled_findings(
&decisions.reader,
&decisions.facts,
legacy_bridges(&scan_view.facts),
)?;
let findings = hits
.iter()
.flat_map(|(term, term_hits)| {
term_hits.iter().map(move |hit| Finding {
modality: modality::PROTECTED_TERM,
location: hit.location.clone(),
evidence: hit.evidence.clone(),
value: term.clone(),
seen_in: Some(hit.seen_in.clone()),
})
})
.chain(unsafe_attribute_hits.iter().map(|hit| Finding {
modality: modality::UNSAFE_ATTRIBUTE_ID,
location: hit.location.clone(),
evidence: hit.evidence.clone(),
value: UNSAFE_ATTRIBUTE_FINDING.to_owned(),
seen_in: Some(hit.seen_in.clone()),
}))
.collect::<Vec<_>>();
let files = [ScannedFile {
path: repo_root.clone(),
outcome: FileOutcome::Examined,
findings,
}];
let target = PathBuf::from(format!("git:{} {range}", repo_root.display()));
let (fragment, scan_id) = build_scan_fragment(
&target,
&files,
&[],
point_interval(now_epoch()?),
channel_id,
if lexical_checked {
BTreeSet::from([modality::PROTECTED_TERM, modality::UNSAFE_ATTRIBUTE_ID])
} else {
BTreeSet::from([modality::UNSAFE_ATTRIBUTE_ID])
},
);
let protected_finding_ids = hits
.values()
.flatten()
.filter_map(|hit| {
findings_at(fragment.facts(), modality::PROTECTED_TERM, &hit.location)
.into_iter()
.min()
.map(|id| (hit.location.clone(), id))
})
.collect::<BTreeMap<_, _>>();
let unsafe_finding_ids = unsafe_attribute_hits
.iter()
.filter_map(|hit| {
findings_at(
fragment.facts(),
modality::UNSAFE_ATTRIBUTE_ID,
&hit.location,
)
.into_iter()
.min()
.map(|id| (hit.location.clone(), id))
})
.collect::<BTreeMap<_, _>>();
storage.publish_scan(fragment, "posture git audit")?;
let found = hits.values().map(Vec::len).sum::<usize>();
let hits = hits
.into_iter()
.filter_map(|(term, term_hits)| {
let kept = term_hits
.into_iter()
.filter(|hit| {
!settled.hides_any(
modality::PROTECTED_TERM,
findings_at(&scan_view.facts, modality::PROTECTED_TERM, &hit.location),
)
})
.collect::<Vec<_>>();
(!kept.is_empty()).then_some((term, kept))
})
.collect::<BTreeMap<_, _>>();
let remaining = hits.values().map(Vec::len).sum::<usize>();
let unsafe_found = unsafe_attribute_hits.len();
let unsafe_attribute_hits = unsafe_attribute_hits
.into_iter()
.filter(|hit| {
!settled.hides_any(
modality::UNSAFE_ATTRIBUTE_ID,
findings_at(
&scan_view.facts,
modality::UNSAFE_ATTRIBUTE_ID,
&hit.location,
),
)
})
.collect::<Vec<_>>();
let unsafe_remaining = unsafe_attribute_hits.len();
let hidden = (found - remaining) + (unsafe_found - unsafe_remaining);
Ok(GitReport {
scan_id,
channel: channel.to_owned(),
protected_terms: terms.len(),
lexical_checked,
range,
commits: n_commits,
added_lines: n_added,
removed_lines: n_removed,
hidden,
hits,
unsafe_attribute_hits,
protected_finding_ids,
unsafe_finding_ids,
})
}
fn install_hooks(
storage: PostureStorage<'_>,
repo: &Path,
executable: &Path,
channel: &str,
remote_match: Option<&str>,
pre_push: bool,
post_commit: bool,
) -> Result<HookReport> {
let (want_pre_push, want_post_commit) = match (pre_push, post_commit) {
(false, false) => (true, true),
pair => pair,
};
let git_dir = {
let out = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-parse", "--git-dir"])
.output()
.map_err(|e| anyhow!("run git: {e}"))?;
if !out.status.success() {
anyhow::bail!("{} is not a git repository", repo.display());
}
let rel = String::from_utf8_lossy(&out.stdout).trim().to_string();
let p = PathBuf::from(&rel);
if p.is_absolute() {
p
} else {
repo.join(p)
}
};
let hooks = git_dir.join("hooks");
std::fs::create_dir_all(&hooks).map_err(|e| anyhow!("create {}: {e}", hooks.display()))?;
let exe = executable.display().to_string();
let pile = storage.storage.path().display().to_string();
let key = storage
.storage
.key_path()
.map(Path::display)
.map(|path| path.to_string())
.unwrap_or_default();
let missing_tooling = |verdict: &str, on_missing_exit: &str| {
format!(
r#"# Fail LEGIBLY. If the binary has been rebuilt away or the pile has moved,
# "command not found" gives no clue why. A safety tool that gets in the way
# without explaining itself gets deleted in irritation, which is the worst
# possible outcome for it.
if [ ! -x "$POSTURE" ]; then
echo "posture: hook installed but the binary is missing at:" >&2
echo " $POSTURE" >&2
echo " Rebuild it, or remove this hook with:" >&2
echo " rm \"$0\"" >&2
echo " {verdict}" >&2
exit {on_missing_exit}
fi
if [ ! -f "$PILE" ]; then
echo "posture: hook installed but the pile is missing at:" >&2
echo " $PILE" >&2
echo " The protected vocabulary lives there, so nothing can be checked." >&2
echo " {verdict}" >&2
exit {on_missing_exit}
fi"#
)
};
let pre_push_script = format!(
r#"#!/bin/sh
# Installed by `posture hook`. Audits what is about to cross into a channel.
# Bypass with --no-verify, but read what it says first.
set -e
POSTURE="{exe}"
PILE="{pile}"
KEY="{key}"
CHANNEL="{channel}"
ZERO=0000000000000000000000000000000000000000
REMOTE_MATCH="{remote_match}"
# git passes the remote NAME as $1 and its URL as $2. A channel describes a
# destination, so a push to a remote this channel does not cover must be skipped
# — auditing a private archive against the public vocabulary is a category
# error, and it blocked a legitimate backup push once. Skipping is announced,
# never silent.
if [ -n "$REMOTE_MATCH" ]; then
case "$2$1" in
*"$REMOTE_MATCH"*) : ;;
*)
echo "posture: remote '$1' does not match '$REMOTE_MATCH' for channel '$CHANNEL' — not audited."
exit 0
;;
esac
fi
{missing}
status=0
# What is already at the remote is not preventable, so the gate does not read
# it. It reads what this push ADDS. For a new branch that is NOT its whole
# history — the branch is new, the commits behind it are mostly not — so the
# selection is "reachable from the pushed ref, not from anything this remote
# already has". Auditing the whole history here made a two-file push demand
# justification for 291 commits and 347k lines of other people's work, and a
# gate nobody can pass is a gate that gets --no-verify'd. History is audited by
# `posture sweep --history`, where it can be worked through rather than
# blocking a push.
if git remote | grep -qx -- "$1"; then
already_there="--remotes=$1"
else
# Pushing to a bare URL: no remote-tracking refs name it, so fall back to
# every remote. Over-auditing here is the safe direction.
already_there="--remotes"
fi
while read -r _local_ref local_sha _remote_ref remote_sha; do
[ "$local_sha" = "$ZERO" ] && continue # branch deletion
if [ "$remote_sha" = "$ZERO" ]; then
# New branch. Deliberately unquoted below: these are shas and ref
# globs, and they have to reach git as separate arguments.
revisions="$local_sha --not $already_there"
else
revisions="$remote_sha..$local_sha"
fi
if [ -n "$KEY" ]; then
PILE="$PILE" TRIBLESPACE_KEY="$KEY" \
"$POSTURE" git --channel "$CHANNEL" $revisions || status=1
else
PILE="$PILE" "$POSTURE" git --channel "$CHANNEL" $revisions || status=1
fi
done
if [ "$status" != 0 ]; then
echo ""
echo "posture: refusing the push. Fix, or re-run with --no-verify if these are"
echo " genuinely fine for the '$CHANNEL' channel."
fi
exit $status
"#,
exe = exe,
pile = pile,
key = key,
channel = channel,
remote_match = remote_match.unwrap_or(""),
missing = missing_tooling(
"Refusing the push rather than passing an unchecked one.",
"1"
),
);
let post_commit_script = format!(
r#"#!/bin/sh
# Installed by `posture hook`. Audits the commit that just happened, so a leak
# is news while `git commit --amend` is still the whole remedy.
#
# ADVISORY. It never fails a commit — the commit already exists, so a non-zero
# exit would change nothing git does and would only train you to ignore it.
# The gate that actually refuses is the pre-push hook.
#
# It does NOT consult a remote-match: a commit has no destination yet, so it
# always audits against '{channel}'. Erring toward telling you is the right
# error for something that cannot block.
POSTURE="{exe}"
PILE="{pile}"
KEY="{key}"
CHANNEL="{channel}"
{missing}
GIT_DIR_PATH=$(git rev-parse --git-dir)
LOG="$GIT_DIR_PATH/posture-post-commit.log"
LOCK="$GIT_DIR_PATH/posture-post-commit.lock"
# Just the commit that was made. `HEAD^@` is all of HEAD's parents, so on a
# merge this is the merge commit alone rather than the entire branch it brought
# in — those commits were news when they were made, and re-announcing them on
# every merge is how an alarm becomes wallpaper. A root commit has no parent to
# subtract.
if git rev-parse --verify --quiet HEAD^1 >/dev/null 2>&1; then
revisions="HEAD --not HEAD^@"
else
revisions="HEAD"
fi
head_sha=$(git rev-parse HEAD)
# `mkdir` is the atomic test-and-set every shell has. Two audits of one pile at
# once would only make both slower; the one already running is announced rather
# than silently dropped, because a smoke alarm that quietly does nothing is
# worse than no smoke alarm.
if ! mkdir "$LOCK" 2>/dev/null; then
echo "posture: an audit of an earlier commit is still running, so $(git rev-parse --short HEAD) was NOT checked."
echo " Re-run it yourself with: posture git HEAD --not HEAD^@ --channel $CHANNEL"
echo " (or remove a stale lock: rmdir \"$LOCK\")"
exit 0
fi
# Detached. Deliberately unquoted revisions: they are revision arguments and
# must reach git separately.
{{
if [ -n "$KEY" ]; then
report=$(PILE="$PILE" TRIBLESPACE_KEY="$KEY" \
"$POSTURE" git --channel "$CHANNEL" $revisions 2>&1)
else
report=$(PILE="$PILE" "$POSTURE" git --channel "$CHANNEL" $revisions 2>&1)
fi
found=$?
rmdir "$LOCK" 2>/dev/null
if [ "$found" = 0 ]; then
# Quiet on a clean commit. The full coverage report after every single
# commit is noise, and noise is how this hook gets removed.
echo ""
echo "posture: nothing flagged in $(git rev-parse --short "$head_sha" 2>/dev/null) for '$CHANNEL' (narrow by"
echo " construction — \`posture git HEAD --not HEAD^@\` prints what it did not check)."
exit 0
fi
echo ""
echo "posture: COMMIT $(git rev-parse --short "$head_sha" 2>/dev/null) carries something the '$CHANNEL' channel protects."
echo ""
printf '%s\n' "$report" | sed 's/^/ /'
echo ""
echo " It is not pushed yet, so the whole remedy is still cheap:"
echo " git commit --amend (fix the content, keep the commit)"
echo " or, if the material is genuinely fine for this channel:"
echo " decide propose \"<what this is>\" --context \"<why it is fine>\" --about <finding id>"
echo " decide resolve <decision> \"<the reasoning>\" --result benign"
echo ""
echo " Nothing is blocked. The pre-push hook is what will refuse."
}} 2>&1 | tee -a "$LOG" &
# Advisory to the end: never non-zero, and never a wait.
exit 0
"#,
exe = exe,
pile = pile,
key = key,
channel = channel,
missing = missing_tooling(
"Reporting nothing rather than pretending this commit was checked.",
"0"
),
);
let mut wanted = Vec::new();
if want_pre_push {
wanted.push(("pre-push", &pre_push_script));
}
if want_post_commit {
wanted.push(("post-commit", &post_commit_script));
}
for (name, _) in &wanted {
refuse_foreign_hook(&hooks, name)?;
}
let installed = wanted
.into_iter()
.map(|(name, script)| write_hook(&hooks, name, script))
.collect::<Result<Vec<_>>>()?;
Ok(HookReport {
installed,
channel: channel.to_owned(),
remote_match: remote_match.map(str::to_owned),
pile: storage.storage.path().to_owned(),
pre_push: want_pre_push,
post_commit: want_post_commit,
})
}
fn refuse_foreign_hook(hooks: &Path, name: &str) -> Result<()> {
let path = hooks.join(name);
if path.exists() {
let existing = std::fs::read_to_string(&path).unwrap_or_default();
if !existing.contains("Installed by `posture hook`") {
anyhow::bail!(
"{} already exists and was not written by posture; refusing to overwrite it",
path.display()
);
}
}
Ok(())
}
fn write_hook(hooks: &Path, name: &str, script: &str) -> Result<PathBuf> {
let path = hooks.join(name);
std::fs::write(&path, script).map_err(|e| anyhow!("write {}: {e}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
.map_err(|e| anyhow!("chmod {}: {e}", path.display()))?;
}
Ok(path)
}
#[cfg(feature = "local-embed")]
use crate::nomic::load_text_embedder;
#[cfg(feature = "local-embed")]
fn l2(mut v: Vec<f32>) -> Vec<f32> {
let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 0.0 {
for x in &mut v {
*x /= n;
}
}
v
}
#[cfg(feature = "local-embed")]
fn chunks(text: &str, lines_per: usize, stride: usize) -> Vec<(usize, String)> {
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
let mut i = 0;
while i < lines.len() {
let end = (i + lines_per).min(lines.len());
let body = lines[i..end].join("\n");
if body.split_whitespace().count() >= 12 {
out.push((i + 1, body));
}
if end == lines.len() {
break;
}
i += stride;
}
out
}
#[cfg(feature = "local-embed")]
fn cmd_exemplar(
storage: PostureStorage<'_>,
text: &str,
channel: &str,
benign: bool,
) -> Result<PolicyReceipt> {
let body = canonical_exemplar(text);
if body.split_whitespace().count() < 12 {
bail!(
"exemplar is too short to embed meaningfully ({} words)",
body.split_whitespace().count()
);
}
let channel_name = canonical_channel(channel)?;
let embedder = load_text_embedder()?;
let vector = l2(embedder
.embed_query(&body)
.map_err(|error| anyhow!("embed exemplar: {error:?}"))?);
let view = storage.policy_view()?;
let mut fragment = Fragment::empty();
let channel_id = append_channel(&mut fragment, &channel_name);
let (head, mut members) = policy_members(&view.facts, channel_id)?;
let replaced = take_exemplars_with_body(&view.reader, &view.facts, &mut members, &body)?;
let text_handle: TextHandle = fragment.put(body.clone());
let role = if benign {
EXEMPLAR_BENIGN
} else {
EXEMPLAR_PROTECTED
};
let exemplar = entity! {
metadata::tag: KIND_EXEMPLAR,
posture::term: text_handle,
posture::in_channel: channel_id,
posture::role: role,
};
let exemplar_id = exemplar.root().expect("intrinsic exemplar has one root");
fragment += exemplar;
let vector_handle = fragment.put(vector);
fragment += entity! {
ExclusiveId::force_ref(&exemplar_id) @
embeddings::attr::embedding: vector_handle
};
members.insert(exemplar_id);
let revision = if replaced != BTreeSet::from([exemplar_id]) {
let predecessors = head.into_iter().collect();
Some(append_policy_revision(
&mut fragment,
channel_id,
&members,
&predecessors,
))
} else {
None
};
let words = body.split_whitespace().count();
if fragment
.facts()
.iter()
.all(|expected| view.facts.iter().any(|actual| &actual == expected))
{
return Ok(PolicyReceipt {
channel_id,
channel: channel_name,
member: exemplar_id,
revision: None,
published: false,
kind: PolicyMemberKind::Exemplar { benign, words },
text: body,
});
}
storage.publish_policy(fragment, "posture policy exemplar")?;
Ok(PolicyReceipt {
channel_id,
channel: channel_name,
member: exemplar_id,
revision,
published: true,
kind: PolicyMemberKind::Exemplar { benign, words },
text: body,
})
}
#[cfg(feature = "local-embed")]
fn semantic_texts(
storage: PostureStorage<'_>,
texts: impl IntoIterator<Item = SemanticDocument>,
channel: &str,
threshold: f32,
mut omissions: Vec<WalkOmission>,
) -> Result<SemanticReport> {
let view = storage.policy_view()?;
let Some(channel_id) = channel_by_name(&view.reader, &view.facts, channel)? else {
bail!("channel {channel:?} has no policy");
};
let (_, members) = policy_members(&view.facts, channel_id)?;
let mut exemplars: Vec<(bool, String, Vec<f32>)> = Vec::new();
let mut n_protected = 0usize;
let mut n_benign = 0usize;
for exemplar in members {
if !exists!(pattern!(&view.facts, [{
(exemplar) @ metadata::tag: (&KIND_EXEMPLAR)
}])) {
continue;
}
let text = one_required(
find!(
value: TextHandle,
pattern!(&view.facts, [{ (exemplar) @ posture::term: ?value }])
)
.collect(),
"exemplar text",
)?;
let role = one_required(
find!(
value: Id,
pattern!(&view.facts, [{ (exemplar) @ posture::role: ?value }])
)
.collect(),
"exemplar role",
)?;
let benign = match role {
EXEMPLAR_BENIGN => {
n_benign += 1;
true
}
EXEMPLAR_PROTECTED => {
n_protected += 1;
false
}
_ => bail!(
"exemplar {} has unknown role {}",
fmt_id(exemplar),
fmt_id(role)
),
};
let vectors = find!(
value: Inline<inlineencodings::Handle<Embedding768>>,
pattern!(&view.facts, [{
(exemplar) @ embeddings::attr::embedding: ?value
}])
)
.collect::<BTreeSet<_>>();
if vectors.is_empty() {
bail!(
"exemplar {} has no embedding exhaust; semantic scan cannot examine it",
fmt_id(exemplar)
);
}
let text = read_text(&view.reader, text, "exemplar text")?;
for vector in vectors {
let vector: View<[f32]> = view
.reader
.get(vector)
.with_context(|| format!("read embedding for exemplar {}", fmt_id(exemplar)))?;
exemplars.push((benign, text.clone(), vector.to_vec()));
}
}
if n_protected == 0 {
anyhow::bail!(
"channel {channel:?} has no protected exemplars — a semantic scan against none \
would report clean while comparing nothing. Add one with \
`posture exemplar @file --channel {channel}`"
);
}
let emb = load_text_embedder()?;
let mut hits: Vec<(f32, PathBuf, usize, String)> = Vec::new();
let mut n_chunks = 0usize;
let mut examined = 0;
let mut skipped = 0;
let mut too_big = 0;
for document in texts {
let (p, text) = match document {
SemanticDocument::Text(path, text) => (path, text),
SemanticDocument::TooBig => {
too_big += 1;
continue;
}
SemanticDocument::Skipped => {
skipped += 1;
continue;
}
SemanticDocument::Omitted(omission) => {
omissions.push(omission);
continue;
}
};
examined += 1;
let mut best: Option<(f32, usize, String)> = None;
for (line, body) in chunks(&text, 12, 8) {
n_chunks += 1;
let v = emb
.embed_query(&body)
.map_err(|error| anyhow!("embed {}:{line}: {error:?}", p.display()))?;
let v = l2(v);
let nearest = |want_benign: bool| {
exemplars
.iter()
.filter(|(b, _, _)| *b == want_benign)
.map(|(_, _, e)| e.iter().zip(&v).map(|(a, b)| a * b).sum::<f32>())
.fold(f32::NEG_INFINITY, f32::max)
};
let protected = nearest(false);
let benign = nearest(true);
let score = if benign.is_finite() {
protected - benign
} else {
protected
};
if best.as_ref().is_none_or(|(b, _, _)| score > *b) {
let snippet = body
.lines()
.map(str::trim)
.max_by_key(|l| {
l.split_whitespace()
.filter(|w| w.chars().any(char::is_alphabetic))
.count()
})
.unwrap_or("")
.to_string();
best = Some((score, line, snippet));
}
}
if let Some((score, line, snip)) = best {
if score >= threshold {
hits.push((score, p.clone(), line, snip));
}
}
}
hits.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
Ok(SemanticReport {
channel: channel.to_owned(),
protected: n_protected,
benign: n_benign,
threshold,
examined,
chunks: n_chunks,
skipped,
too_big,
omissions,
hits: hits
.into_iter()
.map(|(score, path, line, snippet)| SemanticHit {
score,
name: path.to_string_lossy().into_owned(),
line,
snippet,
})
.collect(),
})
}
#[cfg(feature = "local-embed")]
fn cmd_semantic(
storage: PostureStorage<'_>,
root: &Path,
channel: &str,
threshold: f32,
) -> Result<SemanticReport> {
validate_semantic(std::iter::empty(), channel, threshold)?;
let mut files = Vec::new();
let mut omissions = Vec::new();
walk(root, &mut files, &mut omissions);
files.sort();
let texts = files
.into_iter()
.map(|path| match std::fs::metadata(&path) {
Ok(metadata) if metadata.len() > SEMANTIC_MAX_BYTES as u64 => SemanticDocument::TooBig,
Ok(_) => match std::fs::read_to_string(&path) {
Ok(text) if text.len() <= SEMANTIC_MAX_BYTES => SemanticDocument::Text(path, text),
Ok(_) => SemanticDocument::TooBig,
Err(_) => SemanticDocument::Skipped,
},
Err(error) => SemanticDocument::Omitted(WalkOmission {
path,
detail: format!("metadata failed before semantic scan: {error}"),
}),
});
semantic_texts(storage, texts, channel, threshold, omissions)
}
#[cfg(not(feature = "local-embed"))]
fn cmd_exemplar(_: PostureStorage<'_>, _: &str, _: &str, _: bool) -> Result<PolicyReceipt> {
bail!("built without the local-embed feature; the semantic tier is unavailable")
}
#[cfg(not(feature = "local-embed"))]
fn cmd_semantic(_: PostureStorage<'_>, _: &Path, _: &str, _: f32) -> Result<SemanticReport> {
bail!("built without the local-embed feature; the semantic tier is unavailable")
}
fn cmd_sweep(
storage: PostureStorage<'_>,
root: &Path,
channel: &str,
all: bool,
history: bool,
) -> Result<SweepReport> {
let (policy_view, scan_view, decisions) = storage.policy_scan_and_decide_views()?;
let terms = channel_terms_from_view(&policy_view, channel)?
.map(|(_, terms)| terms)
.unwrap_or_default();
let lexical_checked = !terms.is_empty();
let settled = settled_findings(
&decisions.reader,
&decisions.facts,
legacy_bridges(&scan_view.facts),
)?;
let mut repos = Vec::new();
for entry in
std::fs::read_dir(root).with_context(|| format!("read sweep root {}", root.display()))?
{
let path = entry
.with_context(|| format!("read directory entry under {}", root.display()))?
.path();
if path.join(".git").exists() {
repos.push(path);
}
}
repos.sort();
let have_gh = std::process::Command::new("gh")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false);
let mut examined = Vec::new();
let mut skipped_private = 0usize;
let mut no_remote = 0usize;
for repo in &repos {
let Some(origin) = git_probe(repo, &["remote", "get-url", "origin"], &[2])? else {
no_remote += 1;
continue;
};
let slug = origin
.rsplit_once(':')
.map(|(_, suffix)| suffix)
.unwrap_or(&origin)
.trim_end_matches(".git")
.rsplitn(3, '/')
.take(2)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("/");
let visibility = if have_gh {
std::process::Command::new("gh")
.args([
"repo",
"view",
&slug,
"--json",
"visibility",
"-q",
".visibility",
])
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
.unwrap_or_else(|| "UNKNOWN".to_owned())
} else {
"UNKNOWN".to_owned()
};
if visibility == "PRIVATE" && !all {
skipped_private += 1;
continue;
}
let base = if history {
None
} else if git_probe(
repo,
&["rev-parse", "--verify", "--quiet", "origin/main"],
&[1],
)?
.is_some()
{
Some("origin/main")
} else if git_probe(
repo,
&["rev-parse", "--verify", "--quiet", "origin/master"],
&[1],
)?
.is_some()
{
Some("origin/master")
} else if git_probe(repo, &["rev-parse", "--verify", "--quiet", "@{u}"], &[1])?.is_some() {
Some("@{u}")
} else {
None
};
let range = base
.map(|base| format!("{base}..HEAD"))
.unwrap_or_else(|| "HEAD".to_owned());
let revisions = vec![range.clone()];
let ahead = git_required(repo, &["log", "--oneline", &range])?
.lines()
.filter(|line| !line.is_empty())
.count();
if ahead == 0 {
continue;
}
let GitAudit {
hits,
unsafe_attribute_hits,
..
} = collect_hits(repo, &revisions, &terms)?;
let hits = hits
.into_iter()
.filter_map(|(term, term_hits)| {
let kept = term_hits
.into_iter()
.filter(|hit| {
!settled.hides_any(
modality::PROTECTED_TERM,
findings_at(&scan_view.facts, modality::PROTECTED_TERM, &hit.location),
)
})
.collect::<Vec<_>>();
(!kept.is_empty()).then_some((term, kept))
})
.collect::<BTreeMap<_, _>>();
let unsafe_attribute_hits = unsafe_attribute_hits
.into_iter()
.filter(|hit| {
!settled.hides_any(
modality::UNSAFE_ATTRIBUTE_ID,
findings_at(
&scan_view.facts,
modality::UNSAFE_ATTRIBUTE_ID,
&hit.location,
),
)
})
.collect::<Vec<_>>();
examined.push(SweepRepository {
path: repo.clone(),
slug,
visibility,
range,
commits: ahead,
terms: hits
.into_iter()
.map(|(term, hits)| (term, hits.len()))
.collect(),
unsafe_attributes: unsafe_attribute_hits.len(),
});
}
Ok(SweepReport {
channel: channel.to_owned(),
protected_terms: terms.len(),
root: root.to_owned(),
repos: repos.len(),
gh_available: have_gh,
include_private: all,
history,
lexical_checked,
examined,
skipped_private,
no_remote,
})
}
#[cfg(test)]
#[test]
fn policy_and_scan_actions_are_one_commit_a_preparing_reader_observes() {
let directory = tempfile::tempdir().unwrap();
let pile = directory.path().join("posture.pile");
let key = directory.path().join("posture.key");
std::fs::File::create(&pile).unwrap();
crate::storage::initialize_signer(&pile, Some(&key)).unwrap();
let capability = Posture::new(pile, Some(key));
let assert_projected = |scope: Id, expected: Id, kind: Id| {
capability
.storage
.with_pile(|pile, signer| {
let source = open_configured(pile, scope, signer.verifying_key())?;
let succinct = pile.attach::<SuccinctArchiveBlob>(source, ())?;
let rank9 = pile.attach::<Rank9AcceleratedSuccinctArchiveBlob>(source, succinct)?;
let snapshot = pollster::block_on(async {
drop(pile.maintain_attached(succinct, signer).await?);
pile.maintain_attached(rank9, signer).await
})?;
let selected = snapshot.attached(rank9)?;
let facts = selected.facts()?;
assert!(find!(
id: Id,
pattern!(&facts, [{ ?id @ metadata::tag: &kind }])
)
.any(|id| id == expected));
assert_eq!(
source.admitted(&snapshot)?.len(),
1,
"the action remains one COMMIT"
);
Ok(())
})
.unwrap();
};
let policy = capability
.vocab_add("private-example", "public", None)
.unwrap();
assert!(policy.published);
assert_projected(DEFAULT_POLICY_SCOPE_ID, policy.member, KIND_TERM);
let scan = capability
.scan(
"generated input",
&[DocumentInput {
name: "empty.txt",
bytes: b"ordinary generated text",
}],
false,
)
.unwrap();
assert_projected(DEFAULT_SCAN_SCOPE_ID, scan.scan_id.unwrap(), KIND_SCAN);
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;