KeiSeiKit-1.0/_primitives/_rust/ssh-check/src/check.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

213 lines
6.8 KiB
Rust

//! Evaluate the hardened rule matrix against a merged sshd_config view.
use crate::parse::Merged;
use crate::rules::{Expect, Rule};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum Severity {
Ok,
Warn,
Fail,
}
impl Severity {
pub fn label(&self) -> &'static str {
match self {
Severity::Ok => "OK",
Severity::Warn => "WARN",
Severity::Fail => "FAIL",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Finding {
pub directive: String,
pub severity: Severity,
pub source: String,
pub note: String,
}
pub fn evaluate(merged: &Merged, rules: &[Rule]) -> Vec<Finding> {
let mut out = Vec::with_capacity(rules.len());
for r in rules {
out.push(eval_rule(merged, r));
}
out
}
fn eval_rule(merged: &Merged, r: &Rule) -> Finding {
let occ = merged.effective.get(r.directive);
match (occ, r.required) {
(None, true) => Finding {
directive: r.directive.into(),
severity: Severity::Fail,
source: "(missing)".into(),
note: format!("required directive absent — {}", r.rationale),
},
(None, false) => Finding {
directive: r.directive.into(),
severity: Severity::Warn,
source: "(missing)".into(),
note: format!("recommended: {}", r.rationale),
},
(Some(o), _) => {
let ok = value_matches(&o.value, &r.expect);
Finding {
directive: r.directive.into(),
severity: if ok { Severity::Ok } else { Severity::Fail },
source: o.source.clone(),
note: if ok {
"ok".into()
} else {
format!("value '{}' violates policy — {}", o.value, r.rationale)
},
}
}
}
}
fn value_matches(value: &str, expect: &Expect) -> bool {
let v = value.trim().to_ascii_lowercase();
match expect {
Expect::Equals(target) => v == target.to_ascii_lowercase(),
Expect::OneOf(list) => list.iter().any(|s| v == s.to_ascii_lowercase()),
Expect::MaxInt(max) => v.parse::<u32>().map(|n| n <= *max).unwrap_or(false),
Expect::ContainsAll(tokens) => tokens.iter().all(|t| v.contains(&t.to_ascii_lowercase())),
Expect::DeniesAny(tokens) => {
let parts: Vec<&str> = v.split(',').map(str::trim).collect();
!tokens
.iter()
.any(|t| parts.iter().any(|p| p == &t.to_ascii_lowercase()))
}
Expect::AllowedUsersSubset(allow) => {
let parts: Vec<String> = v
.split_whitespace()
.map(|s| s.to_string())
.collect();
!parts.is_empty() && parts.iter().all(|u| allow.contains(u))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::{Merged, Occurrence};
use crate::rules::hardened_matrix;
use std::collections::BTreeMap;
fn merged(pairs: &[(&str, &str)]) -> Merged {
let mut m = Merged {
effective: BTreeMap::new(),
all: BTreeMap::new(),
};
for (k, v) in pairs {
let occ = Occurrence {
value: (*v).to_string(),
source: "test:1".into(),
};
m.effective.insert((*k).to_string(), occ.clone());
m.all.insert((*k).to_string(), vec![occ]);
}
m
}
#[test]
fn hardened_baseline_passes() {
let rules = hardened_matrix(&["keiadmin".into()]);
let mg = merged(&[
("passwordauthentication", "no"),
("permitrootlogin", "prohibit-password"),
("maxauthtries", "3"),
("allowusers", "keiadmin"),
("ciphers", "chacha20-poly1305@openssh.com,aes256-gcm@openssh.com"),
("macs", "hmac-sha2-512-etm@openssh.com"),
("hostkeyalgorithms", "ssh-ed25519,rsa-sha2-512"),
]);
let findings = evaluate(&mg, &rules);
let fails: Vec<_> = findings.iter().filter(|f| f.severity == Severity::Fail).collect();
assert!(fails.is_empty(), "unexpected fails: {fails:#?}");
}
#[test]
fn password_auth_yes_fails() {
let rules = hardened_matrix(&["keiadmin".into()]);
let mg = merged(&[
("passwordauthentication", "yes"),
("permitrootlogin", "no"),
("maxauthtries", "3"),
("allowusers", "keiadmin"),
]);
let findings = evaluate(&mg, &rules);
let f = findings
.iter()
.find(|f| f.directive == "passwordauthentication")
.unwrap();
assert_eq!(f.severity, Severity::Fail);
}
#[test]
fn cbc_cipher_fails() {
let rules = hardened_matrix(&["keiadmin".into()]);
let mg = merged(&[
("passwordauthentication", "no"),
("permitrootlogin", "no"),
("maxauthtries", "3"),
("allowusers", "keiadmin"),
("ciphers", "aes256-cbc,chacha20-poly1305@openssh.com"),
]);
let findings = evaluate(&mg, &rules);
let f = findings.iter().find(|f| f.directive == "ciphers").unwrap();
assert_eq!(f.severity, Severity::Fail);
}
#[test]
fn allow_users_not_in_whitelist_fails() {
let rules = hardened_matrix(&["keiadmin".into()]);
let mg = merged(&[
("passwordauthentication", "no"),
("permitrootlogin", "no"),
("maxauthtries", "3"),
("allowusers", "root attacker"),
]);
let findings = evaluate(&mg, &rules);
let f = findings.iter().find(|f| f.directive == "allowusers").unwrap();
assert_eq!(f.severity, Severity::Fail);
}
#[test]
fn missing_required_directive_fails() {
let rules = hardened_matrix(&["keiadmin".into()]);
let mg = merged(&[
("permitrootlogin", "no"),
("maxauthtries", "3"),
("allowusers", "keiadmin"),
]);
let findings = evaluate(&mg, &rules);
let f = findings
.iter()
.find(|f| f.directive == "passwordauthentication")
.unwrap();
assert_eq!(f.severity, Severity::Fail);
assert_eq!(f.source, "(missing)");
}
#[test]
fn maxauthtries_too_high_fails() {
let rules = hardened_matrix(&["keiadmin".into()]);
let mg = merged(&[
("passwordauthentication", "no"),
("permitrootlogin", "no"),
("maxauthtries", "10"),
("allowusers", "keiadmin"),
]);
let findings = evaluate(&mg, &rules);
let f = findings
.iter()
.find(|f| f.directive == "maxauthtries")
.unwrap();
assert_eq!(f.severity, Severity::Fail);
}
}