cc_audit/parser/
dockerfile.rs1use super::traits::{ContentParser, ContentType, ParsedContent};
4use crate::error::Result;
5use serde_json::json;
6
7pub struct DockerfileParser;
9
10impl DockerfileParser {
11 pub fn new() -> Self {
13 Self
14 }
15
16 pub fn extract_base_images(content: &str) -> Vec<String> {
18 content
19 .lines()
20 .filter_map(|line| {
21 let trimmed = line.trim();
22 if trimmed.to_uppercase().starts_with("FROM ") {
23 let parts: Vec<&str> = trimmed[5..].split_whitespace().collect();
24 parts.first().map(|s| s.to_string())
25 } else {
26 None
27 }
28 })
29 .collect()
30 }
31
32 pub fn extract_run_commands(content: &str) -> Vec<String> {
34 let mut commands = Vec::new();
35 let mut in_run = false;
36 let mut current_command = String::new();
37
38 for line in content.lines() {
39 let trimmed = line.trim();
40
41 if in_run {
42 if let Some(stripped) = trimmed.strip_suffix('\\') {
44 current_command.push_str(stripped);
45 current_command.push(' ');
46 } else {
47 current_command.push_str(trimmed);
48 commands.push(current_command.clone());
49 current_command.clear();
50 in_run = false;
51 }
52 } else if trimmed.to_uppercase().starts_with("RUN ") {
53 let cmd = &trimmed[4..];
54 if let Some(stripped) = cmd.strip_suffix('\\') {
55 current_command = stripped.to_string();
56 current_command.push(' ');
57 in_run = true;
58 } else {
59 commands.push(cmd.to_string());
60 }
61 }
62 }
63
64 if in_run && !current_command.is_empty() {
66 commands.push(current_command);
67 }
68
69 commands
70 }
71
72 pub fn extract_env_vars(content: &str) -> Vec<(String, String)> {
74 content
75 .lines()
76 .filter_map(|line| {
77 let trimmed = line.trim();
78 if trimmed.to_uppercase().starts_with("ENV ") {
79 let rest = trimmed[4..].trim();
80 if let Some(eq_idx) = rest.find('=') {
82 let key = rest[..eq_idx].trim().to_string();
83 let value = rest[eq_idx + 1..].trim().to_string();
84 Some((key, value))
85 } else {
86 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
87 if parts.len() == 2 {
88 Some((parts[0].to_string(), parts[1].to_string()))
89 } else {
90 None
91 }
92 }
93 } else {
94 None
95 }
96 })
97 .collect()
98 }
99}
100
101impl Default for DockerfileParser {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl ContentParser for DockerfileParser {
108 fn parse(&self, content: &str, path: &str) -> Result<ParsedContent> {
109 let base_images = Self::extract_base_images(content);
110 let run_commands = Self::extract_run_commands(content);
111 let env_vars = Self::extract_env_vars(content);
112
113 let structured = json!({
114 "base_images": base_images,
115 "run_commands": run_commands,
116 "env_vars": env_vars.iter().map(|(k, v)| json!({k: v})).collect::<Vec<_>>(),
117 });
118
119 let parsed = ParsedContent::new(
120 ContentType::Dockerfile,
121 content.to_string(),
122 path.to_string(),
123 )
124 .with_structured_data(structured);
125
126 Ok(parsed)
127 }
128
129 fn supported_extensions(&self) -> &[&str] {
130 &["dockerfile"]
131 }
132
133 fn can_parse(&self, path: &str) -> bool {
134 let filename = std::path::Path::new(path)
135 .file_name()
136 .and_then(|n| n.to_str())
137 .unwrap_or("");
138
139 let lower = filename.to_lowercase();
140 lower == "dockerfile" || lower.starts_with("dockerfile.")
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_extract_base_images() {
150 let content = r#"
151FROM node:18-alpine AS builder
152RUN npm install
153FROM nginx:latest
154COPY --from=builder /app/dist /usr/share/nginx/html
155"#;
156 let images = DockerfileParser::extract_base_images(content);
157 assert_eq!(images, vec!["node:18-alpine", "nginx:latest"]);
158 }
159
160 #[test]
161 fn test_extract_run_commands() {
162 let content = r#"
163FROM alpine
164RUN apk add --no-cache curl
165RUN npm install && \
166 npm run build
167"#;
168 let commands = DockerfileParser::extract_run_commands(content);
169 assert_eq!(commands.len(), 2);
170 assert!(commands[0].contains("apk add"));
171 assert!(commands[1].contains("npm install") && commands[1].contains("npm run build"));
172 }
173
174 #[test]
175 fn test_extract_env_vars() {
176 let content = r#"
177FROM alpine
178ENV NODE_ENV=production
179ENV APP_PORT 3000
180"#;
181 let vars = DockerfileParser::extract_env_vars(content);
182 assert_eq!(vars.len(), 2);
183 assert!(vars.contains(&("NODE_ENV".to_string(), "production".to_string())));
184 assert!(vars.contains(&("APP_PORT".to_string(), "3000".to_string())));
185 }
186
187 #[test]
188 fn test_parse_dockerfile() {
189 let parser = DockerfileParser::new();
190 let content = r#"
191FROM node:18-alpine
192ENV NODE_ENV=production
193RUN npm install
194"#;
195 let result = parser.parse(content, "Dockerfile").unwrap();
196
197 assert_eq!(result.content_type, ContentType::Dockerfile);
198 assert!(result.structured_data.is_some());
199 let data = result.structured_data.unwrap();
200 assert!(!data["base_images"].as_array().unwrap().is_empty());
201 }
202
203 #[test]
204 fn test_can_parse() {
205 let parser = DockerfileParser::new();
206 assert!(parser.can_parse("Dockerfile"));
207 assert!(parser.can_parse("dockerfile"));
208 assert!(parser.can_parse("Dockerfile.prod"));
209 assert!(!parser.can_parse("docker-compose.yml"));
210 }
211}