Coverage Gap
Coverage gap analysis finds entities that are missing a qualifying neighbor. This answers questions like:
- "Which hosts have no EDR agent protecting them?"
- "Which services have no scanner scanning them?"
- "Which databases have no backup agent connected to them?"
Basic Usage
#![allow(unused)] fn main() { let graph = GraphReader::new(&snap); // Find all hosts with no EDR agent protecting them let unprotected = graph .coverage_gap("PROTECTS") .target_type("host") .neighbor_type("edr_agent") .find(); println!("{} hosts have no EDR coverage", unprotected.len()); }
CoverageGapBuilder
#![allow(unused)] fn main() { impl<'snap> CoverageGapBuilder<'snap> { /// The relationship verb that represents coverage. /// e.g., "PROTECTS", "SCANS", "MANAGES" /// (This is the first argument to coverage_gap()) /// The type of entity to check for coverage gaps. pub fn target_type(self, t: &str) -> Self; /// The class of entity that provides coverage. pub fn target_class(self, c: &str) -> Self; /// The type of entity that provides coverage (the "covering" entity). pub fn neighbor_type(self, t: &str) -> Self; /// The class of the covering entity. pub fn neighbor_class(self, c: &str) -> Self; /// Direction of the coverage edge (default: Incoming — neighbor → target). pub fn direction(self, dir: Direction) -> Self; /// Execute and return all entities with no qualifying neighbor. pub fn find(self) -> Vec<&'snap Entity>; } }
INV-G06
INV-G06: Coverage gap only returns entities of target_type that have
no qualifying neighbor via the specified verb. Entities that have at least
one qualifying neighbor are excluded.
Examples
Scanner coverage
#![allow(unused)] fn main() { // Which hosts have never been scanned? let unscanned = graph .coverage_gap("SCANS") .target_type("host") .neighbor_class("Scanner") .direction(Direction::Incoming) // scanner → host .find(); }
EDR protection
#![allow(unused)] fn main() { // Which containers have no security agent? let unprotected = graph .coverage_gap("PROTECTS") .target_class("Container") .neighbor_class("Agent") .find(); }
Backup coverage
#![allow(unused)] fn main() { // Which databases have no backup relationship? let unbackedup = graph .coverage_gap("HAS") .target_class("Database") .neighbor_type("backup_job") .find(); }
PQL Equivalent
Coverage gap corresponds to PQL's negated traversal:
-- PQL: find hosts with no EDR protection
FIND host THAT !PROTECTS edr_agent
-- Rust equivalent
graph.coverage_gap("PROTECTS").target_type("host").neighbor_type("edr_agent").find()
Performance
Coverage gap requires:
- Fetch all entities of
target_type(index scan) - For each entity, check if any
verbedge exists to a qualifying neighbor (adjacency lookup)
The adjacency index makes step 2 O(degree) per entity, not O(n_relationships). For 10,000 hosts with average degree 5, expect:
- 10,000 adjacency lookups × O(5) = 50,000 operations
- Typically completes in <50ms