///| A named audit policy for a class of deployments.
pub struct AuditPolicy {
name : String
rules : Array[Rule]
expected_keys : Array[String]
forbidden_keys : Array[String]
sensitive_keys : Array[String]
} derive(@debug.Debug, Eq)
///| A compact health summary for a configuration view.
pub struct AuditSummary {
policy_name : String
key_count : Int
error_count : Int
warning_count : Int
info_count : Int
score : Int
diagnostics : Array[Diagnostic]
} derive(@debug.Debug, Eq)
pub fn audit_policy(
name : String,
rules : Array[Rule],
expected_keys? : Array[String] = [],
forbidden_keys? : Array[String] = [],
sensitive_keys? : Array[String] = [],
) -> AuditPolicy {
{ name, rules, expected_keys, forbidden_keys, sensitive_keys }
}
///| Runs a policy and computes a coarse 0-100 health score.
pub fn audit(view : ConfigView, policy : AuditPolicy) -> AuditSummary {
let diagnostics : Array[Diagnostic] = []
append_diagnostics(diagnostics, validate(view, policy.rules))
append_diagnostics(diagnostics, audit_expected_keys(view, policy.expected_keys))
append_diagnostics(diagnostics, audit_forbidden_keys(view, policy.forbidden_keys))
append_diagnostics(diagnostics, audit_sensitive_keys(view, policy.sensitive_keys))
append_diagnostics(diagnostics, audit_parse_diagnostics(view))
let errors = count_severity(diagnostics, Error)
let warnings = count_severity(diagnostics, Warning)
let infos = count_severity(diagnostics, Info)
let score = clamp_score(100 - errors * 25 - warnings * 8 - infos * 1)
{
policy_name: policy.name,
key_count: view.values.length(),
error_count: errors,
warning_count: warnings,
info_count: infos,
score,
diagnostics,
}
}
fn audit_expected_keys(view : ConfigView, keys : Array[String]) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for path in keys {
if view.get_path(path) == None {
diagnostics.push(warning("expected-key", "expected key '" + path + "' is absent"))
}
}
diagnostics
}
fn audit_forbidden_keys(view : ConfigView, keys : Array[String]) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for path in keys {
match view.get_path(path) {
None => ()
Some(_) => diagnostics.push(error("forbidden-key", "forbidden key '" + path + "' is present"))
}
}
diagnostics
}
fn audit_sensitive_keys(view : ConfigView, keys : Array[String]) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for path in keys {
match view.get_path(path) {
None => ()
Some(value) => {
if value == "" {
diagnostics.push(error("secret-empty", "sensitive key '" + path + "' is empty"))
} else if value.length() < 12 {
diagnostics.push(warning("secret-weak", "sensitive key '" + path + "' is short for production use"))
}
}
}
}
diagnostics
}
fn audit_parse_diagnostics(view : ConfigView) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for item in view.diagnostics {
diagnostics.push(item)
}
diagnostics
}
fn count_severity(diagnostics : Array[Diagnostic], severity : Severity) -> Int {
let mut count = 0
for item in diagnostics {
if item.severity == severity {
count = count + 1
}
}
count
}
fn clamp_score(score : Int) -> Int {
if score < 0 {
0
} else if score > 100 {
100
} else {
score
}
}
///| Renders the audit summary as Markdown.
pub fn render_audit_summary(summary : AuditSummary) -> String {
let builder = StringBuilder()
builder.write_string("# Audit Summary: " + summary.policy_name + "\n\n")
builder.write_string("- Score: " + summary.score.to_string() + "/100\n")
builder.write_string("- Keys: " + summary.key_count.to_string() + "\n")
builder.write_string("- Errors: " + summary.error_count.to_string() + "\n")
builder.write_string("- Warnings: " + summary.warning_count.to_string() + "\n")
builder.write_string("- Info: " + summary.info_count.to_string() + "\n\n")
if summary.diagnostics.length() == 0 {
builder.write_string("No audit diagnostics.\n")
} else {
builder.write_string("## Findings\n\n")
for diagnostic in summary.diagnostics {
builder.write_string("- " + audit_severity_label(diagnostic.severity) + " `" + diagnostic.code + "` " + diagnostic.message + "\n")
}
}
builder.to_string()
}
fn audit_severity_label(severity : Severity) -> String {
match severity {
Error => "ERROR"
Warning => "WARN"
Info => "INFO"
}
}
///| A strict policy suitable for production services.
pub fn production_service_policy() -> AuditPolicy {
audit_policy(
"production-service",
[
required("server.host"),
required("server.port"),
required("database.url"),
enum_value("server.mode", ["production"]),
int_range("server.port", Some(1), Some(65535)),
int_range("server.workers", Some(1), Some(256)),
int_range("database.pool_min", Some(0), Some(1024)),
int_range("database.pool_max", Some(1), Some(4096)),
secret_like("database.password"),
secret_like("auth.token"),
conflicts("auth.oauth", "auth.basic"),
],
expected_keys=[
"server.request_timeout_ms",
"database.pool_max",
"auth.enabled",
],
forbidden_keys=[
"debug",
"server.debug",
"auth.legacy",
"database.plaintext_password",
],
sensitive_keys=[
"database.password",
"auth.token",
"auth.client_secret",
],
)
}
///| A relaxed policy for local development.
pub fn development_service_policy() -> AuditPolicy {
audit_policy(
"development-service",
[
required("server.host"),
required("server.port"),
enum_value("server.mode", ["development", "test", "production"]),
int_range("server.port", Some(1), Some(65535)),
],
expected_keys=[
"server.mode",
"database.url",
],
forbidden_keys=[],
sensitive_keys=[
"auth.token",
],
)
}
///| A policy for CI test fixture configs.
pub fn ci_fixture_policy() -> AuditPolicy {
audit_policy(
"ci-fixture",
[
required("fixture.root"),
int_range("test.retries", Some(0), Some(10)),
int_range("fixture.seed", Some(0), None),
enum_value("output.format", ["text", "json", "markdown"]),
],
expected_keys=[
"test.pattern",
"fixture.reset",
],
forbidden_keys=[
"database.password",
"auth.token",
],
sensitive_keys=[],
)
}