///|
pub(all) struct PathQuery {
source : String
sink : String
include_allowed : Bool
maximum_paths : Int
} derive(Eq, Debug, ToJson)
///|
pub(all) struct PathSummary {
source : String
sink : String
path_count : Int
allowed_count : Int
unsafe_count : Int
shortest_length : Int
longest_length : Int
paths : Array[Array[String]]
} derive(Eq, Debug, ToJson)
///|
pub(all) struct NodeImpact {
node : String
kind : String
paths_from_sources : Int
paths_to_sinks : Int
policy_count : Int
is_control_point : Bool
impact_score : Int
} derive(Eq, Debug, ToJson)
///|
pub fn default_path_query(source : String, sink : String) -> PathQuery {
{ source, sink, include_allowed: false, maximum_paths: 100 }
}
///|
pub fn query_paths(model : Model, query : PathQuery) -> PathSummary {
let all_paths = find_paths(model, query.source, query.sink)
let selected : Array[Array[String]] = []
let mut allowed_count = 0
let mut unsafe_count = 0
let mut shortest_length = 0
let mut longest_length = 0
for path in all_paths {
let allowed = is_allowed(model, path)
if allowed {
allowed_count += 1
} else {
unsafe_count += 1
}
if query.include_allowed || !allowed {
if query.maximum_paths <= 0 || selected.length() < query.maximum_paths {
selected.push(path)
}
}
if shortest_length == 0 || path.length() < shortest_length {
shortest_length = path.length()
}
if path.length() > longest_length {
longest_length = path.length()
}
}
{
source: query.source,
sink: query.sink,
path_count: all_paths.length(),
allowed_count,
unsafe_count,
shortest_length,
longest_length,
paths: selected,
}
}
///|
pub fn query_paths_json(summary : PathSummary) -> String {
summary.to_json().stringify(indent=2)
}
///|
pub fn format_path_summary(summary : PathSummary) -> String {
let out = StringBuilder()
out.write_string("paths \{summary.source} -> \{summary.sink}")
out.write_string(" total=\{summary.path_count}")
out.write_string(" allowed=\{summary.allowed_count}")
out.write_string(" unsafe=\{summary.unsafe_count}")
out.write_string(" shortest=\{summary.shortest_length}")
out.write_string(" longest=\{summary.longest_length}")
for path in summary.paths {
let path_text = path.join(" -> ")
out.write_string("\n- \{path_text}")
}
out.to_string()
}
///|
pub fn rank_node_impact(model : Model) -> Array[NodeImpact] {
let result : Array[NodeImpact] = []
for node in model.nodes {
let from_sources = count_source_paths(model, node.name)
let to_sinks = count_sink_paths(model, node.name)
let policy_count = count_policies_for_node(model, node.name)
let control = node.kind == Sanitizer || node.kind == Boundary
let impact_score = from_sources * to_sinks +
policy_count * 3 +
(if control { 5 } else { 0 })
result.push({
node: node.name,
kind: node_kind_name(node.kind),
paths_from_sources: from_sources,
paths_to_sinks: to_sinks,
policy_count,
is_control_point: control,
impact_score,
})
}
sort_impacts(result)
result
}
///|
pub fn find_nodes_by_text(model : Model, query : String) -> Array[Node] {
let result : Array[Node] = []
for node in model.nodes {
if contains_text(node.name, query) || contains_text(node.description, query) {
result.push(node)
}
}
result
}
///|
pub fn nodes_for_policy(model : Model, policy_index : Int) -> Array[String] {
if policy_index < 0 || policy_index >= model.policies.length() {
return []
}
let result : Array[String] = []
let policy = model.policies[policy_index]
for name in policy.path {
if !result.contains(name) {
result.push(name)
}
}
if policy.through != "" && !result.contains(policy.through) {
result.push(policy.through)
}
result
}
///|
pub fn query_policy_impact(model : Model, node_name : String) -> Array[Policy] {
let result : Array[Policy] = []
for policy in model.policies {
if path_contains(policy.path, node_name) || policy.through == node_name {
result.push(policy)
}
}
result
}
///|
pub fn query_source_sink_pairs(model : Model) -> Array[(String, String)] {
let result : Array[(String, String)] = []
let sources = graph_node_names_by_kind(model, Source)
let sinks = graph_node_names_by_kind(model, Sink)
for source in sources {
for sink in sinks {
if has_path(model, source, sink) {
result.push((source, sink))
}
}
}
result
}
///|
pub fn query_reachable_nodes(model : Model, start : String) -> Array[String] {
let result : Array[String] = []
let queue : Array[String] = [start]
for ;; {
if queue.length() == 0 {
break
}
let current = queue.remove(0)
if result.contains(current) {
continue
}
result.push(current)
for edge in model.edges {
if edge.from == current && !result.contains(edge.to) {
queue.push(edge.to)
}
}
}
result
}
///|
fn count_source_paths(model : Model, target : String) -> Int {
let mut count = 0
for source in graph_node_names_by_kind(model, Source) {
if has_path(model, source, target) {
count += 1
}
}
count
}
///|
fn count_sink_paths(model : Model, start : String) -> Int {
let mut count = 0
for sink in graph_node_names_by_kind(model, Sink) {
if has_path(model, start, sink) {
count += 1
}
}
count
}
///|
fn count_policies_for_node(model : Model, node_name : String) -> Int {
let mut count = 0
for policy in model.policies {
if path_contains(policy.path, node_name) || policy.through == node_name {
count += 1
}
}
count
}
///|
fn contains_text(text : String, query : String) -> Bool {
if query == "" {
return true
}
text.contains(query)
}
///|
fn sort_impacts(items : Array[NodeImpact]) -> Unit {
let mut index = 0
while index < items.length() {
let mut best = index
let mut candidate = index + 1
while candidate < items.length() {
if items[candidate].impact_score > items[best].impact_score {
best = candidate
}
candidate += 1
}
if best != index {
let current = items[index]
items[index] = items[best]
items[best] = current
}
index += 1
}
}