///|
pub(all) struct WitnessGroup {
kind : ChangeKind
action : String
resource_kind : String
cross_tenant : Bool
before_rules : Array[String]
after_rules : Array[String]
occurrences : Int
example : AccessRequest
} derive(Debug)
///|
pub(all) struct WitnessSelection {
groups : Array[WitnessGroup]
total_changed_requests : Int
omitted_groups : Int
complete : Bool
} derive(Debug)
///|
fn resource_kind(universe : RequestUniverse, id : String) -> String {
for resource in universe.resources {
if resource.id == id {
return resource.kind
}
}
""
}
///|
fn same_signature(a : WitnessGroup, b : WitnessGroup) -> Bool {
a.kind == b.kind &&
a.action == b.action &&
a.resource_kind == b.resource_kind &&
a.cross_tenant == b.cross_tenant &&
a.before_rules == b.before_rules &&
a.after_rules == b.after_rules
}
///|
/// Picks one representative request per decision signature. The first
/// request in input order is retained. This is a compact review view, while
/// the full audit report remains the source of all individual changes.
pub fn select_witnesses(
report : AuditReport,
universe : RequestUniverse,
max_groups : Int,
) -> WitnessSelection {
let groups : Array[WitnessGroup] = []
if !report.complete || max_groups < 1 {
return {
groups,
total_changed_requests: 0,
omitted_groups: 0,
complete: false,
}
}
let mut total_changed = 0
let mut omitted = 0
let suppressed_signatures : Array[WitnessGroup] = []
for change in report.changes {
if !(change.kind is NewGrant ||
change.kind is RevokedGrant ||
change.kind is Inconclusive) {
continue
}
total_changed += 1
let source_tenant = principal_tenant(universe, change.request.principal_id)
let target_tenant = resource_tenant(universe, change.request.resource_id)
let candidate : WitnessGroup = {
kind: change.kind,
action: change.request.action,
resource_kind: resource_kind(universe, change.request.resource_id),
cross_tenant: source_tenant != target_tenant,
before_rules: change.before.decisive_rules,
after_rules: change.after.decisive_rules,
occurrences: 1,
example: change.request,
}
let mut index = -1
for i, existing in groups {
if same_signature(existing, candidate) {
index = i
break
}
}
if index >= 0 {
groups[index] = {
..groups[index],
occurrences: groups[index].occurrences + 1,
}
} else if groups.length() < max_groups {
groups.push(candidate)
} else {
let mut seen = false
for suppressed in suppressed_signatures {
if same_signature(suppressed, candidate) {
seen = true
break
}
}
if !seen {
suppressed_signatures.push(candidate)
omitted += 1
}
}
}
{
groups,
total_changed_requests: total_changed,
omitted_groups: omitted,
complete: omitted == 0,
}
}
///|
pub fn render_witnesses(selection : WitnessSelection) -> String {
let out = StringBuilder()
out.write_string("Changed requests: \{selection.total_changed_requests}\n")
out.write_string("Representative groups: \{selection.groups.length()}\n")
out.write_string("Complete grouping: \{selection.complete}\n")
for group in selection.groups {
out.write_string(
"\{change_name(group.kind)} \{group.action} \{group.resource_kind} cross-tenant=\{group.cross_tenant} count=\{group.occurrences}\n",
)
out.write_string(
" witness: \{group.example.principal_id} \{group.example.action} \{group.example.resource_id}\n",
)
out.write_string(" old rules: ")
write_ids(out, group.before_rules)
out.write_string("\n new rules: ")
write_ids(out, group.after_rules)
out.write_string("\n")
}
if selection.omitted_groups > 0 {
out.write_string("Omitted distinct groups: \{selection.omitted_groups}\n")
}
out.to_string()
}