syncable_cli/analyzer/dclint/rules/
dcl005.rs1use crate::analyzer::dclint::rules::{LintContext, Rule, SimpleRule, make_failure};
6use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
7
8const CODE: &str = "DCL005";
9const NAME: &str = "no-unbound-port-interfaces";
10const DESCRIPTION: &str = "Ports should bind to a specific interface for security.";
11const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-unbound-port-interfaces-rule.md";
12
13pub fn rule() -> impl Rule {
14 SimpleRule::new(
15 CODE,
16 NAME,
17 Severity::Warning,
18 RuleCategory::Security,
19 DESCRIPTION,
20 URL,
21 check,
22 )
23}
24
25fn check(ctx: &LintContext) -> Vec<CheckFailure> {
26 let mut failures = Vec::new();
27
28 for (service_name, service) in &ctx.compose.services {
29 for port in &service.ports {
30 if port.host_port.is_some() && !port.has_explicit_interface() {
32 let message = format!(
33 "Port \"{}\" in service \"{}\" does not specify a host interface. Consider binding to 127.0.0.1 for local-only access.",
34 port.raw, service_name
35 );
36
37 failures.push(
38 make_failure(
39 &CODE.into(),
40 NAME,
41 Severity::Warning,
42 RuleCategory::Security,
43 message,
44 port.position.line,
45 port.position.column,
46 false,
47 )
48 .with_data("serviceName", service_name.clone())
49 .with_data("port", port.raw.clone()),
50 );
51 }
52 }
53 }
54
55 failures
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::analyzer::dclint::parser::parse_compose;
62
63 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
64 let compose = parse_compose(yaml).unwrap();
65 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
66 check(&ctx)
67 }
68
69 #[test]
70 fn test_no_violation_explicit_interface() {
71 let yaml = r#"
72services:
73 web:
74 image: nginx
75 ports:
76 - "127.0.0.1:8080:80"
77"#;
78 assert!(check_yaml(yaml).is_empty());
79 }
80
81 #[test]
82 fn test_no_violation_container_only() {
83 let yaml = r#"
84services:
85 web:
86 image: nginx
87 ports:
88 - 80
89"#;
90 assert!(check_yaml(yaml).is_empty());
92 }
93
94 #[test]
95 fn test_violation_unbound_port() {
96 let yaml = r#"
97services:
98 web:
99 image: nginx
100 ports:
101 - "8080:80"
102"#;
103 let failures = check_yaml(yaml);
104 assert_eq!(failures.len(), 1);
105 assert!(failures[0].message.contains("8080:80"));
106 assert!(failures[0].message.contains("127.0.0.1"));
107 }
108
109 #[test]
110 fn test_multiple_violations() {
111 let yaml = r#"
112services:
113 web:
114 image: nginx
115 ports:
116 - "8080:80"
117 - "127.0.0.1:8443:443"
118 - "3000:3000"
119"#;
120 let failures = check_yaml(yaml);
121 assert_eq!(failures.len(), 2); }
123}