Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
839 changes: 834 additions & 5 deletions drift v3/crates/drift-engine/src/check_command.rs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions drift v3/crates/drift-engine/src/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ pub enum FactKind {
RouteDeclared,
FileRoleDetected,
TestDeclared,
AuthGuardCalled,
RouteReturnsResponse,
CallbackBoundaryDetected,
MiddlewareDeclared,
MiddlewareMatcherDeclared,
MiddlewareProtectsRoute,
RequestInputRead,
RequestValidationCalled,
ValidatedInputUsed,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
34 changes: 34 additions & 0 deletions drift v3/crates/drift-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ pub const DRIFT_ENGINE_VERSION: &str = "0.1.0";
mod diff;
mod facts;
mod rules;
mod security_capabilities;
mod security_control_flow;
mod security_facts;
mod security_patterns;
mod security_proof;
mod security_rules;

use std::{
fs::File,
Expand All @@ -23,6 +29,34 @@ pub use rules::{
Severity, classify_findings_against_baseline, detect_direct_data_access_imports,
materialize_direct_data_access_findings,
};
pub use security_capabilities::{
SecurityCapabilityStatus, SecurityScanCapability, security_capabilities,
};
pub use security_control_flow::{
MatchedMiddleware, MiddlewareMismatch, ValidatedInputUse, static_middleware_coverage,
validated_input_uses,
};
pub use security_facts::extract_security_facts;
pub use security_facts::extract_security_facts_with_validation;
pub use security_patterns::{
AcceptedAuthHelper, AcceptedRequestValidator, AuthGuardBehavior, RequestValidatorBehavior,
RequestValidatorKind, dynamic_middleware_matcher_line,
};
pub use security_proof::{
AuthBoundaryProof, MiddlewareBoundaryProof, RequestInputReadProof, RequestUnvalidatedUseProof,
RequestValidatedUseProof, RequestValidationCallProof, RequestValidationProof,
RouteSecurityBoundaryProof, SecurityBoundaryProof, SecurityParserGap, SecurityProofResult,
SecurityProofStatus, TrustedGuardCallProof, UndominatedSinkProof, build_auth_boundary_proof,
build_auth_boundary_proofs_for_file, build_middleware_coverage_proof,
build_request_validation_proof,
};
pub use security_rules::{
SecurityAuthContract, SecurityContractCapability, SecurityEnforcementMode, SecurityFinding,
SecurityFindingResult, SecurityMiddlewareContract, SecurityRequestValidationContract,
evaluate_api_route_requires_auth_helper,
evaluate_api_route_requires_auth_helper_with_middleware,
evaluate_api_route_requires_request_validation, evaluate_middleware_must_cover_routes,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileFingerprint {
Expand Down
134 changes: 128 additions & 6 deletions drift v3/crates/drift-engine/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ mod protocol;

use candidate_command::infer_candidates;
use check_command::check_repo;
use drift_engine::{Fact, FactKind, extract_typescript_facts, should_index_path};
use drift_engine::{
Fact, FactKind, dynamic_middleware_matcher_line, extract_security_facts,
extract_typescript_facts, should_index_path, static_middleware_coverage,
};
use protocol::*;
use serde_json::json;
use sha2::{Digest, Sha256};
Expand Down Expand Up @@ -152,6 +155,8 @@ fn scan_repo(
let mut graph_edge_count = 0_usize;
let scanned = scan_files(repo_root, &files, &mut diagnostics, reuse_index.as_ref())?;
let files_reused = scanned.files_reused;
let mut scanned = scanned;
add_middleware_coverage_facts(&mut scanned.scanned);
resolver.exported_symbols = exported_symbols_by_file(&scanned.scanned);
for (file, file_facts) in scanned.scanned {
let graph = graph_for_file(&repo_id, &scan_id, &file, &file_facts, &resolver);
Expand Down Expand Up @@ -221,12 +226,13 @@ fn stream_scan_repo(
let mut graph_edges_emitted = 0_usize;
let mut diagnostics_emitted = 0_usize;
let mut scan_diagnostics = Vec::new();
let scanned = scan_files(
let mut scanned = scan_files(
repo_root,
&files,
&mut scan_diagnostics,
reuse_index.as_ref(),
)?;
add_middleware_coverage_facts(&mut scanned.scanned);
files_skipped += scan_diagnostics.len();
if !scan_diagnostics.is_empty() {
diagnostics_emitted += scan_diagnostics.len();
Expand Down Expand Up @@ -406,10 +412,17 @@ fn scan_file_with_reuse(
return Ok(Some((file, reused_facts, true)));
}
let source = fs::read_to_string(&absolute_path)?;
let facts = extract_typescript_facts(file_path, &source)?
.into_iter()
.map(engine_fact)
.collect();
if is_middleware_path(&normalized) && dynamic_middleware_matcher_line(&source).is_some() {
diagnostics.push(EngineDiagnostic {
severity: "warning".to_string(),
code: "unsupported_dynamic_middleware_matcher".to_string(),
message: "unsupported_dynamic_middleware_matcher".to_string(),
file_path: Some(normalized.clone()),
});
}
let mut facts = extract_typescript_facts(file_path, &source)?;
facts.extend(extract_security_facts(file_path, &source, &[])?);
let facts = facts.into_iter().map(engine_fact).collect();
Ok(Some((file, facts, false)))
}

Expand Down Expand Up @@ -451,6 +464,106 @@ fn reusable_facts_for_file(
)
}

fn add_middleware_coverage_facts(scanned: &mut [(ScannedFile, Vec<EngineFact>)]) {
let middleware_fact_sets = scanned
.iter()
.filter_map(|(_, facts)| {
if !facts.iter().any(|fact| fact.kind == "middleware_declared") {
return None;
}
Some(
facts
.iter()
.filter_map(middleware_fact_from_engine)
.collect::<Vec<_>>(),
)
})
.filter(|facts| !facts.is_empty())
.collect::<Vec<_>>();
if middleware_fact_sets.is_empty() {
return;
}

for (_, route_facts) in scanned.iter_mut() {
if !route_facts
.iter()
.any(|fact| fact.kind == "file_role_detected" && fact.name == "api_route")
{
continue;
}
let route_file_path = route_facts
.iter()
.find(|fact| fact.kind == "route_declared")
.map(|fact| fact.file_path.clone())
.unwrap_or_else(|| {
route_facts
.first()
.map(|fact| fact.file_path.clone())
.unwrap_or_default()
});
let route_method = route_facts
.iter()
.find(|fact| fact.kind == "route_declared")
.map(|fact| fact.name.as_str())
.unwrap_or("GET");
let route_line = route_facts
.iter()
.find(|fact| fact.kind == "route_declared")
.map(|fact| fact.start_line)
.unwrap_or(1);
let route_id = format!("route:{route_file_path}:{route_method}");
let mut new_facts = Vec::new();
for middleware_facts in &middleware_fact_sets {
let (matched, _) =
static_middleware_coverage(middleware_facts, &route_file_path, route_method);
for middleware in matched {
let protection_kind = middleware.protection_kind.clone();
new_facts.push(EngineFact {
kind: "middleware_protects_route".to_string(),
file_path: route_file_path.clone(),
name: middleware.middleware_id.clone(),
value: Some(
json!({
"route_id": route_id,
"middleware_id": middleware.middleware_id,
"protection_kind": protection_kind,
})
.to_string(),
),
imported_name: Some(protection_kind),
start_line: route_line,
end_line: route_line,
});
}
}
route_facts.extend(new_facts);
}
}

fn middleware_fact_from_engine(fact: &EngineFact) -> Option<Fact> {
let kind = match fact.kind.as_str() {
"middleware_declared" => FactKind::MiddlewareDeclared,
"middleware_matcher_declared" => FactKind::MiddlewareMatcherDeclared,
_ => return None,
};
Some(Fact {
kind,
file_path: fact.file_path.clone(),
name: fact.name.clone(),
value: fact.value.clone(),
imported_name: fact.imported_name.clone(),
start_line: fact.start_line,
end_line: fact.end_line,
})
}

fn is_middleware_path(path: &str) -> bool {
path == "middleware.ts"
|| path == "middleware.js"
|| path.ends_with("/middleware.ts")
|| path.ends_with("/middleware.js")
}

fn reused_file(file: &ScannedFile, reuse: Option<&ReuseIndex>) -> bool {
reusable_facts_for_file(file, reuse).is_some()
}
Expand Down Expand Up @@ -618,6 +731,15 @@ fn fact_kind(kind: FactKind) -> &'static str {
FactKind::RouteDeclared => "route_declared",
FactKind::FileRoleDetected => "file_role_detected",
FactKind::TestDeclared => "test_declared",
FactKind::AuthGuardCalled => "auth_guard_called",
FactKind::RouteReturnsResponse => "route_returns_response",
FactKind::CallbackBoundaryDetected => "callback_boundary_detected",
FactKind::MiddlewareDeclared => "middleware_declared",
FactKind::MiddlewareMatcherDeclared => "middleware_matcher_declared",
FactKind::MiddlewareProtectsRoute => "middleware_protects_route",
FactKind::RequestInputRead => "request_input_read",
FactKind::RequestValidationCalled => "request_validation_called",
FactKind::ValidatedInputUsed => "validated_input_used",
}
}

Expand Down
25 changes: 25 additions & 0 deletions drift v3/crates/drift-engine/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ pub struct CheckGraphData {
#[derive(Debug, Deserialize)]
pub struct CheckRepoContext {
pub repo_id: String,
#[serde(default)]
pub repo_root: Option<String>,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -308,14 +310,30 @@ pub struct EngineCandidateEvidenceRef {

#[derive(Debug, Deserialize)]
pub struct CheckContract {
#[serde(default)]
pub contract_id: Option<String>,
#[serde(default)]
pub contract_schema_version: Option<usize>,
pub conventions: Vec<CheckConvention>,
#[serde(default)]
pub waivers: Vec<Value>,
#[serde(default)]
pub exceptions: Vec<Value>,
}

#[derive(Debug, Deserialize)]
pub struct CheckConvention {
pub id: String,
pub kind: String,
pub matcher: CheckMatcher,
#[serde(default)]
pub requires: Option<Value>,
#[serde(default)]
pub scope: Option<Value>,
#[serde(default)]
pub exceptions: Vec<Value>,
#[serde(default)]
pub governance: Option<Value>,
pub severity: String,
pub enforcement_mode: String,
pub enforcement_capability: String,
Expand All @@ -325,6 +343,8 @@ pub struct CheckConvention {
pub struct CheckMatcher {
pub forbidden_imports: Option<Vec<String>>,
pub allowed_delegate_imports: Option<Vec<String>>,
pub required_calls: Option<Vec<String>>,
pub applies_to_file_roles: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -356,6 +376,8 @@ pub struct CheckResult {
pub adapter_versions: BTreeMap<String, String>,
pub diff_mode: String,
pub findings: Vec<CheckFinding>,
#[serde(default)]
pub security_boundary_proofs: Vec<Value>,
pub diagnostics: Vec<EngineDiagnostic>,
pub stats: EngineStats,
pub completeness: Vec<EngineCompleteness>,
Expand Down Expand Up @@ -499,12 +521,15 @@ pub fn capability_stats(required: &[&str], missing: &[&str]) -> EngineCapability
pub fn certified_capabilities() -> Vec<String> {
[
"candidate_inference",
"auth_boundary_facts",
"control_flow_guard_dominance",
"data_operation_detection",
"direct_data_access_check",
"file_discovery",
"graph_stream",
"import_resolution",
"route_detection",
"security_facts",
"symbol_linking",
"syntax_facts",
]
Expand Down
42 changes: 42 additions & 0 deletions drift v3/crates/drift-engine/src/security_capabilities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityCapabilityStatus {
Complete,
Partial,
Unsupported,
Failed,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecurityScanCapability {
pub name: String,
pub capability: String,
pub status: SecurityCapabilityStatus,
pub can_block: bool,
pub block_requires_accepted_convention: bool,
}

pub fn security_capabilities() -> Vec<SecurityScanCapability> {
vec![
SecurityScanCapability {
name: "security_facts".to_string(),
capability: "deterministic_check".to_string(),
status: SecurityCapabilityStatus::Partial,
can_block: false,
block_requires_accepted_convention: true,
},
SecurityScanCapability {
name: "auth_boundary_facts".to_string(),
capability: "deterministic_check".to_string(),
status: SecurityCapabilityStatus::Partial,
can_block: true,
block_requires_accepted_convention: true,
},
SecurityScanCapability {
name: "control_flow_guard_dominance".to_string(),
capability: "deterministic_check".to_string(),
status: SecurityCapabilityStatus::Partial,
can_block: true,
block_requires_accepted_convention: true,
},
]
}
Loading
Loading