///|
/// Audit advice and heuristics; these are not parser errors.
pub(all) enum AuditIssueKind {
RelativeTypeUri
RelativeInstanceUri
SuspiciousExtensionName
OverlongTitle
OverlongDetail
AboutBlankTitleMismatch
MissingTitle
MissingStatus
SensitiveLookingExtension
DebugInformationRisk
} derive(Eq, Debug)
///|
pub(all) enum AuditSeverity {
Info
Warning
} derive(Eq, Debug)
///|
pub struct AuditIssue {
kind : AuditIssueKind
severity : AuditSeverity
context : String
} derive(Eq, Debug)
///|
pub struct AuditReport {
issues : Array[AuditIssue]
} derive(Eq, Debug)
///|
pub fn AuditIssue::kind(self : AuditIssue) -> AuditIssueKind {
self.kind
}
///|
pub fn AuditIssue::severity(self : AuditIssue) -> AuditSeverity {
self.severity
}
///|
pub fn AuditIssue::context(self : AuditIssue) -> String {
self.context
}
///|
pub fn AuditReport::issues(self : AuditReport) -> Array[AuditIssue] {
self.issues.copy()
}
///|
pub fn AuditReport::is_clean(self : AuditReport) -> Bool {
self.issues.is_empty()
}
///|
pub fn AuditIssueKind::name(self : AuditIssueKind) -> String {
match self {
RelativeTypeUri => "RelativeTypeUri"
RelativeInstanceUri => "RelativeInstanceUri"
SuspiciousExtensionName => "SuspiciousExtensionName"
OverlongTitle => "OverlongTitle"
OverlongDetail => "OverlongDetail"
AboutBlankTitleMismatch => "AboutBlankTitleMismatch"
MissingTitle => "MissingTitle"
MissingStatus => "MissingStatus"
SensitiveLookingExtension => "SensitiveLookingExtension"
DebugInformationRisk => "DebugInformationRisk"
}
}
///|
/// Heuristic security and interoperability audit. It makes no leak-detection guarantee.
pub fn audit_problem(problem : ProblemDetails) -> AuditReport {
let issues : Array[AuditIssue] = []
match problem.type_raw() {
Some(uri) =>
match validate_uri_reference(uri) {
Ok(info) if info.is_relative() =>
push_audit(
issues,
RelativeTypeUri,
"type is a relative URI-reference",
)
_ => ()
}
None => ()
}
match problem.instance() {
Some(uri) =>
match validate_uri_reference(uri) {
Ok(info) if info.is_relative() =>
push_audit(
issues,
RelativeInstanceUri,
"instance is a relative URI-reference",
)
_ => ()
}
None => ()
}
match problem.title() {
None => push_audit(issues, MissingTitle, "title is absent")
Some(title) => {
if utf8_length(title) > 256 {
push_audit(issues, OverlongTitle, "title exceeds 256 UTF-8 bytes")
}
if problem.is_about_blank() {
match problem.status() {
Some(code) =>
match status_reason(code) {
Some(reason) if title != reason =>
push_audit(
issues,
AboutBlankTitleMismatch,
"about:blank title differs from the common reason phrase; localization may explain this",
)
_ => ()
}
None => ()
}
}
}
}
match problem.detail() {
Some(detail) => {
if utf8_length(detail) > 4_096 {
push_audit(issues, OverlongDetail, "detail exceeds 4096 UTF-8 bytes")
}
let lowered = ascii_lower(detail)
if lowered.contains("stack trace") ||
lowered.contains("select * from") ||
lowered.contains("exception at") {
push_audit(
issues,
DebugInformationRisk,
"detail resembles debug information",
)
}
}
None => ()
}
if problem.status() is None {
push_audit(issues, MissingStatus, "status is absent")
}
for extension in problem.extensions() {
if suspicious_extension_name(extension.name()) {
push_audit(
issues,
SuspiciousExtensionName,
"extension name is outside the recommended portable form: \{extension.name()}",
)
}
if sensitive_name(extension.name()) {
push_audit(
issues,
SensitiveLookingExtension,
"extension name may indicate sensitive or debug data: \{extension.name()}",
)
}
}
{ issues, }
}
///|
fn push_audit(
issues : Array[AuditIssue],
kind : AuditIssueKind,
context : String,
) -> Unit {
issues.push({ kind, severity: Warning, context })
}
///|
fn suspicious_extension_name(name : String) -> Bool {
let chars = name.to_array()
if chars.length() < 3 || !is_alpha(chars[0]) {
return true
}
for c in chars {
if !is_alpha(c) && !is_digit(c) && c != '_' {
return true
}
}
false
}
///|
fn sensitive_name(name : String) -> Bool {
let value = ascii_lower(name)
value == "stack" ||
value == "stacktrace" ||
value == "exception" ||
value == "sql" ||
value == "query" ||
value == "internal_error" ||
value == "debug" ||
value == "trace" ||
value == "token" ||
value == "secret" ||
value == "password"
}