///|
pub(all) struct QualityGateOptions {
minimum_coverage_percent : Int
maximum_risk_score : Int
reject_unreachable_policies : Bool
reject_invalid_severity : Bool
require_source_and_sink : Bool
} derive(Eq, Debug, ToJson)
///|
pub(all) struct QualityGateResult {
passed : Bool
reasons : Array[String]
assessment : ModelAssessment
policy_audit : PolicyAudit
finding_count : Int
high_finding_count : Int
checked_source_sink_pairs : Int
} derive(Eq, Debug, ToJson)
///|
pub(all) struct WorkflowRun {
source : String
model : Model
findings : Array[Finding]
assessment : ModelAssessment
policy_audit : PolicyAudit
quality_gate : QualityGateResult
sarif : String
} derive(Eq, Debug, ToJson)
///|
pub fn default_quality_gate_options() -> QualityGateOptions {
{
minimum_coverage_percent: 80,
maximum_risk_score: 60,
reject_unreachable_policies: true,
reject_invalid_severity: true,
require_source_and_sink: true,
}
}
///|
pub fn run_quality_gate(model : Model) -> QualityGateResult {
run_quality_gate_with_options(model, default_quality_gate_options())
}
///|
pub fn run_quality_gate_with_options(
model : Model,
options : QualityGateOptions,
) -> QualityGateResult {
let assessment = assess_model(model)
let policy_audit = audit_policies(model)
let findings = analyze(model)
let reasons : Array[String] = []
if options.require_source_and_sink && assessment.metrics.source_count == 0 {
reasons.push("model has no source node")
}
if options.require_source_and_sink && assessment.metrics.sink_count == 0 {
reasons.push("model has no sink node")
}
if policy_audit.coverage_percent < options.minimum_coverage_percent {
reasons.push("policy coverage is below the configured gate")
}
if assessment.risk_score > options.maximum_risk_score {
reasons.push("risk score exceeds the configured gate")
}
if options.reject_unreachable_policies && policy_audit.unreachable_count > 0 {
reasons.push("one or more policies have unreachable endpoints")
}
if options.reject_invalid_severity && has_invalid_severity(model) {
reasons.push("one or more policies use an invalid severity")
}
{
passed: reasons.length() == 0,
reasons,
assessment,
policy_audit,
finding_count: findings.length(),
high_finding_count: count_findings_by_severity(findings, "high"),
checked_source_sink_pairs: query_source_sink_pairs(model).length(),
}
}
///|
pub fn run_workflow(source : String, model : Model) -> WorkflowRun {
let findings = analyze(model)
let assessment = assess_model(model)
let policy_audit = audit_policies(model)
let quality_gate = run_quality_gate(model)
{
source,
model,
findings,
assessment,
policy_audit,
quality_gate,
sarif: format_sarif(findings),
}
}
///|
pub fn run_workflow_text(
source : String,
input : String,
) -> Result[WorkflowRun, TrustFlowError] {
match parse_model(input) {
Ok(model) => Ok(run_workflow(source, model))
Err(error) => Err(error)
}
}
///|
pub fn workflow_json(run : WorkflowRun) -> String {
run.to_json().stringify(indent=2)
}
///|
pub fn format_quality_gate(result : QualityGateResult) -> String {
let out = StringBuilder()
out.write_string("quality_gate={result.passed}")
out.write_string(" risk={result.assessment.risk_score}")
out.write_string(" coverage={result.policy_audit.coverage_percent}%")
out.write_string(" pairs={result.checked_source_sink_pairs}")
for reason in result.reasons {
out.write_string("\n- \{reason}")
}
out.to_string()
}
///|
pub fn workflow_is_release_ready(run : WorkflowRun) -> Bool {
run.quality_gate.passed && run.quality_gate.high_finding_count == 0
}
///|
pub fn workflow_changed_since_baseline(
baseline : Array[BaselineFinding],
run : WorkflowRun,
) -> BaselineComparison {
compare_baseline(baseline, run.findings)
}
///|
fn count_findings_by_severity(
findings : Array[Finding],
severity : String,
) -> Int {
let mut count = 0
for finding in findings {
if finding.severity == severity {
count += 1
}
}
count
}
///|
fn has_invalid_severity(model : Model) -> Bool {
for policy in model.policies {
if policy.severity != "high" &&
policy.severity != "medium" &&
policy.severity != "low" &&
policy.severity != "info" {
return true
}
}
false
}