///| A named environment profile.
pub struct Profile {
name : String
view : ConfigView
schema : ConfigSchema?
policy : AuditPolicy?
} derive(@debug.Debug, Eq)
///| Summary row for comparing profiles.
pub struct ProfileSummary {
name : String
key_count : Int
missing_required : Int
audit_score : Int?
sensitive_count : Int
} derive(@debug.Debug, Eq)
pub fn profile(name : String, view : ConfigView, schema? : ConfigSchema? = None, policy? : AuditPolicy? = None) -> Profile {
{ name, view, schema, policy }
}
pub fn summarize_profile(profile : Profile) -> ProfileSummary {
let missing_required = match profile.schema {
None => 0
Some(spec) => profile_gap_report(spec, [(profile.name, profile.view)]).length()
}
let audit_score = match profile.policy {
None => None
Some(policy) => Some(audit(profile.view, policy).score)
}
{
name: profile.name,
key_count: profile.view.values.length(),
missing_required,
audit_score,
sensitive_count: sensitive_paths(profile.view).length(),
}
}
pub fn summarize_profiles(profiles : Array[Profile]) -> Array[ProfileSummary] {
let summaries : Array[ProfileSummary] = []
for item in profiles {
summaries.push(summarize_profile(item))
}
summaries
}
pub fn render_profile_summary(profiles : Array[Profile]) -> String {
let builder = StringBuilder()
builder.write_string("| Profile | Keys | Missing Required | Audit Score | Sensitive Keys |\n")
builder.write_string("| --- | --- | --- | --- | --- |\n")
for summary in summarize_profiles(profiles) {
builder.write_string("| " + summary.name + " | " + summary.key_count.to_string() + " | " + summary.missing_required.to_string() + " | " + score_text(summary.audit_score) + " | " + summary.sensitive_count.to_string() + " |\n")
}
builder.to_string()
}
fn score_text(score : Int?) -> String {
match score {
None => ""
Some(value) => value.to_string()
}
}
///| Finds keys whose values differ across profiles.
pub fn profile_value_differences(profiles : Array[Profile]) -> Array[String] {
let paths = collect_profile_object_paths(profiles)
let diff_paths : Array[String] = []
for path in paths {
if profile_path_differs(profiles, path) {
diff_paths.push(path)
}
}
diff_paths
}
fn collect_profile_object_paths(profiles : Array[Profile]) -> Array[String] {
let paths : Array[String] = []
for item in profiles {
for value in item.view.values {
let path = entry_path(value.section, value.key)
if !profile_path_contains(paths, path) {
paths.push(path)
}
}
}
paths
}
fn profile_path_contains(paths : Array[String], target : String) -> Bool {
for path in paths {
if path == target {
return true
}
}
false
}
fn profile_path_differs(profiles : Array[Profile], path : String) -> Bool {
let mut seen = false
let mut first = ""
for item in profiles {
match item.view.get_path(path) {
None => {
if seen {
return true
} else {
first = ""
seen = true
}
}
Some(value) => {
if !seen {
first = value
seen = true
} else if value != first {
return true
}
}
}
}
false
}
pub fn render_profile_diff_summary(profiles : Array[Profile]) -> String {
let builder = StringBuilder()
builder.write_string("# Profile Differences\n\n")
let paths = profile_value_differences(profiles)
if paths.length() == 0 {
builder.write_string("No profile value differences.\n")
} else {
builder.write_string("| Path |")
for item in profiles {
builder.write_string(" " + item.name + " |")
}
builder.write_string("\n| --- |")
for _ in profiles {
builder.write_string(" --- |")
}
builder.write_string("\n")
for path in paths {
builder.write_string("| " + path + " |")
for item in profiles {
match item.view.get_path(path) {
None => builder.write_string(" |")
Some(value) => builder.write_string(" " + redact_value(path, value) + " |")
}
}
builder.write_string("\n")
}
}
builder.to_string()
}
///| Creates common dev/test/prod profiles from documents.
pub fn service_profiles(dev : ConfigDocument, test_doc : ConfigDocument, prod : ConfigDocument) -> Array[Profile] {
let spec = web_service_schema()
[
profile("dev", merge_layers([layer("dev", 0, dev)]), schema=Some(spec), policy=Some(development_service_policy())),
profile("test", merge_layers([layer("test", 0, test_doc)]), schema=Some(spec), policy=Some(development_service_policy())),
profile("prod", merge_layers([layer("prod", 0, prod)]), schema=Some(spec), policy=Some(production_service_policy())),
]
}
///| Checks whether a production profile is ready enough for deployment.
pub fn deployment_ready(profile : Profile) -> Bool {
match profile.policy {
None => summarize_profile(profile).missing_required == 0
Some(policy) => {
let summary = audit(profile.view, policy)
summary.error_count == 0 && summary.score >= 80
}
}
}
pub fn render_deployment_check(profile : Profile) -> String {
let builder = StringBuilder()
builder.write_string("# Deployment Check: " + profile.name + "\n\n")
if deployment_ready(profile) {
builder.write_string("Status: ready\n")
} else {
builder.write_string("Status: blocked\n")
}
match profile.policy {
None => ()
Some(policy) => builder.write_string(render_audit_summary(audit(profile.view, policy)))
}
builder.to_string()
}