///|
/// A named user-agent and path probe.
pub struct AccessProbe {
name : String
agent : String
path : String
} derive(Eq, Debug)
///|
pub fn access_probe(
name : String,
agent : String,
path : String,
) -> AccessProbe {
{ name, agent, path }
}
///|
pub fn AccessProbe::name(self : AccessProbe) -> String {
self.name
}
///|
pub fn AccessProbe::agent(self : AccessProbe) -> String {
self.agent
}
///|
pub fn AccessProbe::path(self : AccessProbe) -> String {
self.path
}
///|
/// Result of running one access probe.
pub struct ProbeOutcome {
probe : AccessProbe
decision : Decision
} derive(Eq, Debug)
///|
pub fn ProbeOutcome::probe(self : ProbeOutcome) -> AccessProbe {
self.probe
}
///|
pub fn ProbeOutcome::decision(self : ProbeOutcome) -> Decision {
self.decision
}
///|
pub fn ProbeOutcome::allowed(self : ProbeOutcome) -> Bool {
self.decision.allowed
}
///|
pub fn ProbeOutcome::summary(self : ProbeOutcome) -> String {
"\{self.probe.name}: \{self.decision.summary()}"
}
///|
/// Deterministic batch of access decisions.
pub struct DecisionMatrix {
outcomes : Array[ProbeOutcome]
allowed_count : Int
denied_count : Int
default_count : Int
} derive(Eq, Debug)
///|
pub fn DecisionMatrix::outcomes(self : DecisionMatrix) -> Array[ProbeOutcome] {
self.outcomes
}
///|
pub fn DecisionMatrix::allowed_count(self : DecisionMatrix) -> Int {
self.allowed_count
}
///|
pub fn DecisionMatrix::denied_count(self : DecisionMatrix) -> Int {
self.denied_count
}
///|
pub fn DecisionMatrix::default_count(self : DecisionMatrix) -> Int {
self.default_count
}
///|
pub fn DecisionMatrix::total(self : DecisionMatrix) -> Int {
self.outcomes.length()
}
///|
pub fn DecisionMatrix::all_allowed(self : DecisionMatrix) -> Bool {
self.denied_count == 0
}
///|
/// Evaluates a list of named probes against one policy.
pub fn run_matrix(
policy : Policy,
probes : Array[AccessProbe],
) -> DecisionMatrix {
let outcomes : Array[ProbeOutcome] = []
let mut allowed_count = 0
let mut denied_count = 0
let mut default_count = 0
for probe in probes {
let decision = decide(policy, probe.agent, probe.path)
outcomes.push({ probe, decision })
if decision.allowed {
allowed_count = allowed_count + 1
} else {
denied_count = denied_count + 1
}
if decision.line == 0 {
default_count = default_count + 1
}
}
{ outcomes, allowed_count, denied_count, default_count }
}
///|
pub fn build_probe_grid(
agents : Array[String],
paths : Array[String],
) -> Array[AccessProbe] {
let probes : Array[AccessProbe] = []
for agent in agents {
for path in paths {
probes.push({ name: "\{agent} \{path}", agent, path })
}
}
probes
}
///|
pub fn find_outcome(matrix : DecisionMatrix, name : String) -> ProbeOutcome? {
for outcome in matrix.outcomes {
if outcome.probe.name == name {
return Some(outcome)
}
}
None
}
///|
pub fn denied_outcomes(matrix : DecisionMatrix) -> Array[ProbeOutcome] {
let output : Array[ProbeOutcome] = []
for outcome in matrix.outcomes {
if !outcome.decision.allowed {
output.push(outcome)
}
}
output
}
///|
pub fn default_outcomes(matrix : DecisionMatrix) -> Array[ProbeOutcome] {
let output : Array[ProbeOutcome] = []
for outcome in matrix.outcomes {
if outcome.decision.line == 0 {
output.push(outcome)
}
}
output
}
///|
fn escape_markdown_cell(value : String) -> String {
value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}
///|
fn escape_csv_cell(value : String) -> String {
if value.contains(",") || value.contains("\"") || value.contains("\n") {
"\"\{value.replace_all(old="\"", new="\"\"")}\""
} else {
value
}
}
///|
pub fn render_matrix_markdown(matrix : DecisionMatrix) -> String {
let output = StringBuilder::new()
output.write_string("| Probe | Agent | Path | Verdict | Rule | Line |\n")
output.write_string("|---|---|---|---|---|---:|\n")
for outcome in matrix.outcomes {
let decision = outcome.decision
let rule = if decision.line == 0 {
"default"
} else {
"\{decision.rule_kind.label()} \{decision.rule_pattern}"
}
output.write_string("| ")
output.write_string(escape_markdown_cell(outcome.probe.name))
output.write_string(" | ")
output.write_string(escape_markdown_cell(outcome.probe.agent))
output.write_string(" | ")
output.write_string(escape_markdown_cell(outcome.probe.path))
output.write_string(" | ")
output.write_string(decision.verdict())
output.write_string(" | ")
output.write_string(escape_markdown_cell(rule))
output.write_string(" | ")
output.write_string("\{decision.line}")
output.write_string(" |\n")
}
output.to_string()
}
///|
pub fn render_matrix_csv(matrix : DecisionMatrix) -> String {
let output = StringBuilder::new()
output.write_string(
"probe,agent,path,allowed,rule_kind,rule_pattern,line,reason\n",
)
for outcome in matrix.outcomes {
let decision = outcome.decision
let row = [
escape_csv_cell(outcome.probe.name),
escape_csv_cell(outcome.probe.agent),
escape_csv_cell(outcome.probe.path),
if decision.allowed {
"true"
} else {
"false"
},
decision.rule_kind.label(),
escape_csv_cell(decision.rule_pattern),
"\{decision.line}",
escape_csv_cell(decision.reason),
]
output.write_string(join_strings(row, ","))
output.write_char('\n')
}
output.to_string()
}
///|
/// Behavior classification used when comparing two matrices.
pub(all) enum BehaviorChange {
UnchangedAllow
UnchangedDeny
BecameAllowed
BecameDenied
MissingBaseline
} derive(Eq, Debug)
///|
pub fn BehaviorChange::label(self : BehaviorChange) -> String {
match self {
UnchangedAllow => "unchanged-allow"
UnchangedDeny => "unchanged-deny"
BecameAllowed => "became-allowed"
BecameDenied => "became-denied"
MissingBaseline => "missing-baseline"
}
}
///|
pub struct MatrixComparison {
name : String
before : ProbeOutcome?
after : ProbeOutcome
change : BehaviorChange
} derive(Eq, Debug)
///|
pub fn MatrixComparison::name(self : MatrixComparison) -> String {
self.name
}
///|
pub fn MatrixComparison::before(self : MatrixComparison) -> ProbeOutcome? {
self.before
}
///|
pub fn MatrixComparison::after(self : MatrixComparison) -> ProbeOutcome {
self.after
}
///|
pub fn MatrixComparison::change(self : MatrixComparison) -> BehaviorChange {
self.change
}
///|
pub fn compare_matrices(
before : DecisionMatrix,
after : DecisionMatrix,
) -> Array[MatrixComparison] {
let output : Array[MatrixComparison] = []
for after_outcome in after.outcomes {
let before_outcome = find_outcome(before, after_outcome.probe.name)
let change = match before_outcome {
None => MissingBaseline
Some(previous) =>
if previous.allowed() && after_outcome.allowed() {
UnchangedAllow
} else if !previous.allowed() && !after_outcome.allowed() {
UnchangedDeny
} else if after_outcome.allowed() {
BecameAllowed
} else {
BecameDenied
}
}
output.push({
name: after_outcome.probe.name,
before: before_outcome,
after: after_outcome,
change,
})
}
output
}
///|
pub fn behavior_flip_count(comparisons : Array[MatrixComparison]) -> Int {
let mut count = 0
for comparison in comparisons {
if comparison.change == BecameAllowed || comparison.change == BecameDenied {
count = count + 1
}
}
count
}
///|
pub fn render_matrix_comparison(
comparisons : Array[MatrixComparison],
) -> String {
let output = StringBuilder::new()
for comparison in comparisons {
output.write_string(comparison.change.label())
output.write_string(": ")
output.write_string(comparison.name)
output.write_char('\n')
}
output.to_string()
}