use crate::rules::types::{Category, Confidence, Rule, Severity};
use regex::Regex;
pub fn rules() -> Vec<Rule> {
vec![
dk_001(),
dk_002(),
dk_003(),
dk_004(),
dk_005(),
dk_006(),
dk_007(),
dk_008(),
]
}
fn dk_001() -> Rule {
Rule {
id: "DK-001",
name: "Privileged container",
description: "Detects privileged mode containers which have full host access",
severity: Severity::Critical,
category: Category::PrivilegeEscalation,
confidence: Confidence::Certain,
patterns: vec![
Regex::new(r"--privileged").expect("DK-001: invalid regex"),
Regex::new(r"privileged:\s*true").expect("DK-001: invalid regex"),
Regex::new(r"(?s)cap_add:.*SYS_ADMIN").expect("DK-001: invalid regex"),
Regex::new(r"--cap-add\s*=?\s*SYS_ADMIN").expect("DK-001: invalid regex"),
Regex::new(r"(?s)cap_add:.*ALL\b").expect("DK-001: invalid regex"),
Regex::new(r"--cap-add\s*=?\s*ALL").expect("DK-001: invalid regex"),
Regex::new(r"-\s*SYS_ADMIN\s*$").expect("DK-001: invalid regex"),
Regex::new(r"-\s*ALL\s*$").expect("DK-001: invalid regex"),
],
exclusions: vec![],
message: "Privileged container detected. This grants full host access and is a major security risk.",
recommendation: "Remove --privileged flag. Use specific capabilities instead of full privileges.",
fix_hint: Some("Remove --privileged. Add only needed caps: --cap-add=NET_ADMIN"),
cwe_ids: &["CWE-250"],
}
}
fn dk_002() -> Rule {
Rule {
id: "DK-002",
name: "Running as root user",
description: "Detects containers that run as root user without explicitly setting a non-root user",
severity: Severity::High,
category: Category::PrivilegeEscalation,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r"(?im)^USER\s+root\s*$").expect("DK-002: invalid regex"),
Regex::new(r"(?m)^USER\s+0\s*$").expect("DK-002: invalid regex"),
Regex::new(r#"user:\s*["']?root["']?"#).expect("DK-002: invalid regex"),
Regex::new(r#"user:\s*["']?0["']?"#).expect("DK-002: invalid regex"),
],
exclusions: vec![
Regex::new(r"^\s*#").expect("DK-002: invalid regex"),
],
message: "Container running as root user detected. This increases the attack surface if container is compromised.",
recommendation: "Add a USER instruction to run as a non-root user. Example: USER nobody or USER 1000:1000",
fix_hint: Some("Add to Dockerfile: RUN useradd -m appuser && USER appuser"),
cwe_ids: &["CWE-250"],
}
}
fn dk_003() -> Rule {
Rule {
id: "DK-003",
name: "Remote script execution in RUN",
description: "Detects curl/wget piped to shell in Dockerfile RUN instructions",
severity: Severity::Critical,
category: Category::SupplyChain,
confidence: Confidence::Certain,
patterns: vec![
Regex::new(r"RUN\s+.*curl\s+[^|]*\|\s*(bash|sh|zsh)").expect("DK-003: invalid regex"),
Regex::new(r"RUN\s+.*wget\s+[^|]*-[a-zA-Z]*O-[^|]*\|\s*(bash|sh|zsh)")
.expect("DK-003: invalid regex"),
Regex::new(r"RUN\s+.*wget\s+[^|]*-O\s*-[^|]*\|\s*(bash|sh|zsh)")
.expect("DK-003: invalid regex"),
Regex::new(r"wget\s+-[a-zA-Z]*O-\s+[^|]*\|\s*(bash|sh)")
.expect("DK-003: invalid regex"),
Regex::new(r"RUN\s+.*curl.*&&\s*(bash|sh)\s").expect("DK-003: invalid regex"),
Regex::new(r"curl\s+-[a-zA-Z]*[sS][a-zA-Z]*\s+[^|]*\|\s*(bash|sh)")
.expect("DK-003: invalid regex"),
],
exclusions: vec![
Regex::new(r"localhost|127\.0\.0\.1").expect("DK-003: invalid regex"),
],
message: "Remote script execution in Dockerfile RUN instruction. This is a supply chain attack vector.",
recommendation: "Download scripts first, verify checksums, then execute. Better: use package managers.",
fix_hint: Some(
"RUN curl -o script.sh URL && echo 'CHECKSUM script.sh' | sha256sum -c && bash script.sh",
),
cwe_ids: &["CWE-829"],
}
}
fn dk_004() -> Rule {
Rule {
id: "DK-004",
name: "ADD from remote URL",
description: "Detects ADD instructions fetching from remote URLs (use COPY instead)",
severity: Severity::High,
category: Category::SupplyChain,
confidence: Confidence::Certain,
patterns: vec![
Regex::new(r"(?m)^ADD\s+https?://").expect("DK-004: invalid regex"),
Regex::new(r"(?m)^ADD\s+ftp://").expect("DK-004: invalid regex"),
],
exclusions: vec![],
message: "ADD from remote URL detected. This bypasses layer caching and may fetch untrusted content.",
recommendation: "Use RUN curl/wget with checksum verification, or COPY from local files.",
fix_hint: Some(
"Replace ADD URL with: RUN curl -o file URL && echo 'checksum file' | sha256sum -c",
),
cwe_ids: &["CWE-829", "CWE-494"],
}
}
fn dk_005() -> Rule {
Rule {
id: "DK-005",
name: "Using latest tag",
description: "Detects use of 'latest' tag which can lead to unpredictable builds",
severity: Severity::Medium,
category: Category::SupplyChain,
confidence: Confidence::Certain,
patterns: vec![
Regex::new(r"(?m)^FROM\s+[^:]+:latest\s*$").expect("DK-005: invalid regex"),
Regex::new(r"(?m)^FROM\s+[^\s:]+\s*$").expect("DK-005: invalid regex"), Regex::new(r#"image:\s*[^:]+:latest\s*$"#).expect("DK-005: invalid regex"),
],
exclusions: vec![Regex::new(r"scratch").expect("DK-005: invalid regex")],
message: "Using 'latest' tag or no tag (defaults to latest). Builds may not be reproducible.",
recommendation: "Pin to a specific version tag or SHA digest for reproducible builds.",
fix_hint: Some("Use specific version: FROM node:20.10.0 or FROM node@sha256:..."),
cwe_ids: &["CWE-1357"],
}
}
fn dk_006() -> Rule {
Rule {
id: "DK-006",
name: "Sensitive port exposed",
description: "Detects exposure of sensitive ports like SSH, database, or admin ports",
severity: Severity::Medium,
category: Category::PrivilegeEscalation,
confidence: Confidence::Tentative,
patterns: vec![
Regex::new(r"EXPOSE\s+22\b").expect("DK-006: invalid regex"),
Regex::new(r"EXPOSE\s+3306\b").expect("DK-006: invalid regex"),
Regex::new(r"EXPOSE\s+5432\b").expect("DK-006: invalid regex"),
Regex::new(r"EXPOSE\s+27017\b").expect("DK-006: invalid regex"),
Regex::new(r"EXPOSE\s+6379\b").expect("DK-006: invalid regex"),
],
exclusions: vec![],
message: "Sensitive port exposed. Database and SSH ports should not be publicly exposed.",
recommendation: "Use internal networks for database connections. Avoid exposing SSH in containers.",
fix_hint: Some(
"Remove EXPOSE for sensitive ports or use Docker networks for internal communication",
),
cwe_ids: &["CWE-200"],
}
}
fn dk_007() -> Rule {
Rule {
id: "DK-007",
name: "HEALTHCHECK disabled",
description: "Detects HEALTHCHECK NONE which disables container health monitoring",
severity: Severity::Low,
category: Category::PrivilegeEscalation,
confidence: Confidence::Certain,
patterns: vec![Regex::new(r"(?im)^HEALTHCHECK\s+NONE\s*$").expect("DK-007: invalid regex")],
exclusions: vec![],
message: "HEALTHCHECK is disabled. Container health cannot be monitored.",
recommendation: "Add a proper HEALTHCHECK instruction to monitor container health.",
fix_hint: Some("Add: HEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1"),
cwe_ids: &["CWE-778"],
}
}
fn dk_008() -> Rule {
Rule {
id: "DK-008",
name: "Host volume mount",
description: "Detects mounting of sensitive host paths into containers",
severity: Severity::High,
category: Category::PrivilegeEscalation,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r"/var/run/docker\.sock").expect("DK-008: invalid regex"),
Regex::new(r#"-v\s+/:/[^/]"#).expect("DK-008: invalid regex"),
Regex::new(r#"volumes:.*\n\s*-\s*/:/[^/]"#).expect("DK-008: invalid regex"),
Regex::new(r#"-v\s+/etc:"#).expect("DK-008: invalid regex"),
Regex::new(r#"-v\s+/proc:"#).expect("DK-008: invalid regex"),
],
exclusions: vec![],
message: "Sensitive host path mounted. This may allow container escape or host compromise.",
recommendation: "Avoid mounting sensitive host paths. Use named volumes or bind mounts to specific directories.",
fix_hint: Some("Use named volumes: -v mydata:/data instead of host paths"),
cwe_ids: &["CWE-250", "CWE-732"],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dk_001_detects_privileged() {
let rule = dk_001();
let test_cases = vec![
("docker run --privileged nginx", true),
("privileged: true", true),
("cap_add: [SYS_ADMIN]", true),
("--cap-add=SYS_ADMIN", true),
("cap_add: [ALL]", true),
("--cap-add ALL", true),
("docker run nginx", false),
("privileged: false", false),
];
for (input, should_match) in test_cases {
let matched = rule.patterns.iter().any(|p| p.is_match(input));
let excluded = rule.exclusions.iter().any(|e| e.is_match(input));
let result = matched && !excluded;
assert_eq!(result, should_match, "Failed for input: {}", input);
}
}
#[test]
fn test_dk_002_detects_root_user() {
let rule = dk_002();
let test_cases = vec![
("USER root", true),
("USER 0", true),
("user: root", true),
("user: \"root\"", true),
("user: 0", true),
("USER nobody", false),
("USER 1000", false),
("user: app", false),
("# USER root", false), ];
for (input, should_match) in test_cases {
let matched = rule.patterns.iter().any(|p| p.is_match(input));
let excluded = rule.exclusions.iter().any(|e| e.is_match(input));
let result = matched && !excluded;
assert_eq!(result, should_match, "Failed for input: {}", input);
}
}
#[test]
fn test_dk_003_detects_curl_pipe_in_run() {
let rule = dk_003();
let test_cases = vec![
("RUN curl -fsSL https://get.docker.com | bash", true),
("RUN wget -qO- https://install.example.com | sh", true),
("curl -sSL https://example.com/install.sh | bash", true),
("RUN apt-get update && apt-get install -y curl", false),
("RUN curl -o script.sh https://example.com/script.sh", false),
(
"RUN curl -fsSL http://localhost:8080/install.sh | bash",
false,
), ];
for (input, should_match) in test_cases {
let matched = rule.patterns.iter().any(|p| p.is_match(input));
let excluded = rule.exclusions.iter().any(|e| e.is_match(input));
let result = matched && !excluded;
assert_eq!(result, should_match, "Failed for input: {}", input);
}
}
#[test]
fn test_dk_001_compose_patterns() {
let rule = dk_001();
let compose_content = r#"
services:
app:
image: nginx
privileged: true
"#;
let matched = rule.patterns.iter().any(|p| p.is_match(compose_content));
assert!(matched, "Should detect privileged: true in compose file");
}
#[test]
fn test_dk_002_dockerfile_patterns() {
let rule = dk_002();
let dockerfile_content = r#"
FROM node:18
WORKDIR /app
USER root
RUN apt-get update
"#;
let matched = rule.patterns.iter().any(|p| p.is_match(dockerfile_content));
assert!(matched, "Should detect USER root in Dockerfile");
}
#[test]
fn snapshot_dk_001() {
let rule = dk_001();
let content = include_str!("../../../tests/fixtures/rules/dk_001.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("dk_001", findings);
}
#[test]
fn snapshot_dk_002() {
let rule = dk_002();
let content = include_str!("../../../tests/fixtures/rules/dk_002.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("dk_002", findings);
}
#[test]
fn snapshot_dk_003() {
let rule = dk_003();
let content = include_str!("../../../tests/fixtures/rules/dk_003.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("dk_003", findings);
}
}