securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
use crate::core::{Finding, Severity};
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use lazy_static::lazy_static;
use regex::Regex;
use std::path::Path;
use std::time::Instant;

lazy_static! {
    /// Dockerfile patterns.
    static ref DOCKERFILE_PATTERNS: Vec<(Regex, &'static str, Severity, &'static str)> = vec![
        (
            Regex::new(r"(?i)^FROM\s+[^\s]+\s+AS\s+\w+|^FROM\s+[^\s]+$").unwrap(),
            "Dockerfile FROM without digest pinning",
            Severity::Low,
            "Use FROM image@sha256:... to pin images and prevent supply chain attacks.",
        ),
        (
            Regex::new(r"(?i)^USER\s+root\s*$").unwrap(),
            "Container running as root",
            Severity::High,
            "Running as root inside a container increases blast radius. Use a non-root user. CWE-250.",
        ),
        (
            Regex::new(r#"(?i)--security-opt\s*=?\s*['"]?no-new-privileges\s*[:=]\s*false"#).unwrap(),
            "no-new-privileges disabled",
            Severity::High,
            "Disabling no-new-privileges allows privilege escalation within the container.",
        ),
        (
            Regex::new(r"(?i)COPY\s+.*\.(pem|key|p12|pfx|jks)\b").unwrap(),
            "Private key/certificate copied into Docker image",
            Severity::Critical,
            "Secrets baked into images persist in layer history. Use runtime secrets instead.",
        ),
        (
            Regex::new(r"(?i)(ENV|ARG)\s+\w*(PASSWORD|SECRET|TOKEN|KEY|CREDENTIAL)\w*\s*=").unwrap(),
            "Secret in Dockerfile ENV/ARG",
            Severity::Critical,
            "Secrets in ENV/ARG are visible in image history. Use runtime mounts or secrets.",
        ),
        (
            Regex::new(r"(?i)curl\s+.*\|\s*(ba)?sh").unwrap(),
            "Pipe curl to shell in Dockerfile",
            Severity::High,
            "Downloading and executing scripts in Dockerfile is a supply chain risk. Download, verify, then execute.",
        ),
        (
            Regex::new(r"(?i)--privileged").unwrap(),
            "Privileged flag in Docker command",
            Severity::Critical,
            "The --privileged flag grants the container full host access. CWE-250.",
        ),
        (
            Regex::new(r"(?i)ADD\s+https?://").unwrap(),
            "Dockerfile ADD from URL",
            Severity::Medium,
            "ADD from URL downloads unverified content. Use COPY with prior verification instead.",
        ),
    ];

    /// docker-compose.yml patterns.
    static ref COMPOSE_PATTERNS: Vec<(Regex, &'static str, Severity, &'static str)> = vec![
        (
            Regex::new(r"(?i)privileged:\s*(true|yes)").unwrap(),
            "Docker Compose privileged container",
            Severity::Critical,
            "Privileged mode grants the container full host kernel access. CWE-250.",
        ),
        (
            Regex::new(r"(?i)/var/run/docker\.sock").unwrap(),
            "Docker socket mounted into container",
            Severity::Critical,
            "Mounting the Docker socket grants full control over the Docker daemon — equivalent to root on host. CWE-269.",
        ),
        (
            Regex::new(r#"(?i)network_mode:\s*['"]?host"#).unwrap(),
            "Container using host network",
            Severity::High,
            "Host network mode exposes all host ports and bypasses network isolation.",
        ),
        (
            Regex::new(r#"(?i)pid:\s*['"]?host"#).unwrap(),
            "Container using host PID namespace",
            Severity::High,
            "Host PID namespace allows the container to see and signal host processes.",
        ),
        (
            Regex::new(r#"(?i)ipc:\s*['"]?host"#).unwrap(),
            "Container using host IPC namespace",
            Severity::High,
            "Host IPC namespace allows shared memory access with host processes.",
        ),
        (
            Regex::new(r"(?i)cap_add:\s*\n\s*-\s*(ALL|SYS_ADMIN|SYS_PTRACE|NET_ADMIN|DAC_READ_SEARCH)").unwrap(),
            "Dangerous Linux capability added to container",
            Severity::Critical,
            "Adding powerful capabilities like SYS_ADMIN or ALL enables container escape. CWE-250.",
        ),
        (
            Regex::new(r"(?i)- SYS_ADMIN").unwrap(),
            "SYS_ADMIN capability added",
            Severity::Critical,
            "SYS_ADMIN enables mount namespace manipulation and container escape.",
        ),
        (
            Regex::new(r"(?i)- ALL\b").unwrap(),
            "ALL capabilities added",
            Severity::Critical,
            "cap_add: ALL grants every Linux capability to the container.",
        ),
        (
            Regex::new(r"(?i)nsenter\s+.*-t\s*(1|\$\(|\`)").unwrap(),
            "nsenter container escape",
            Severity::Critical,
            "nsenter with PID 1 is a container escape technique that enters the host namespaces.",
        ),
    ];

    /// Kubernetes YAML patterns.
    static ref K8S_PATTERNS: Vec<(Regex, &'static str, Severity, &'static str)> = vec![
        (
            Regex::new(r"(?i)privileged:\s*(true|yes)").unwrap(),
            "Kubernetes privileged pod",
            Severity::Critical,
            "Privileged pods have full access to the host kernel. CWE-250.",
        ),
        (
            Regex::new(r"(?i)hostNetwork:\s*(true|yes)").unwrap(),
            "Kubernetes hostNetwork enabled",
            Severity::High,
            "hostNetwork exposes the pod to the host network stack.",
        ),
        (
            Regex::new(r"(?i)hostPID:\s*(true|yes)").unwrap(),
            "Kubernetes hostPID enabled",
            Severity::High,
            "hostPID allows the pod to see host processes.",
        ),
        (
            Regex::new(r"(?i)hostIPC:\s*(true|yes)").unwrap(),
            "Kubernetes hostIPC enabled",
            Severity::High,
            "hostIPC allows shared memory access with host.",
        ),
        (
            Regex::new(r"(?i)hostPath:").unwrap(),
            "Kubernetes hostPath volume mount",
            Severity::High,
            "hostPath mounts can expose sensitive host directories to the pod.",
        ),
        (
            Regex::new(r"(?i)allowPrivilegeEscalation:\s*(true|yes)").unwrap(),
            "Kubernetes allowPrivilegeEscalation enabled",
            Severity::High,
            "Allows processes to gain more privileges than their parent.",
        ),
        (
            Regex::new(r"(?i)readOnlyRootFilesystem:\s*(false|no)").unwrap(),
            "Kubernetes writable root filesystem",
            Severity::Medium,
            "A writable root filesystem increases the attack surface.",
        ),
        (
            Regex::new(r"(?i)runAsUser:\s*0\b").unwrap(),
            "Kubernetes pod running as root (UID 0)",
            Severity::High,
            "Running as root inside a pod increases blast radius.",
        ),
        (
            Regex::new(r"(?i)automountServiceAccountToken:\s*(true|yes)").unwrap(),
            "Kubernetes service account token auto-mounted",
            Severity::Medium,
            "Auto-mounted service account tokens can be used for lateral movement.",
        ),
        // RBAC escalation patterns
        (
            Regex::new(r#"(?i)verbs:\s*\[?"?\*"?\]?"#).unwrap(),
            "Kubernetes RBAC wildcard verb",
            Severity::Critical,
            "Wildcard verbs grant all actions on the specified resources. CWE-269.",
        ),
        (
            Regex::new(r#"(?i)resources:\s*\[?"?\*"?\]?"#).unwrap(),
            "Kubernetes RBAC wildcard resource",
            Severity::Critical,
            "Wildcard resources with broad verbs grants cluster-admin-equivalent access.",
        ),
        (
            Regex::new(r"(?i)verbs:.*\b(escalate|bind|impersonate)\b").unwrap(),
            "Kubernetes RBAC privilege escalation verb",
            Severity::Critical,
            "escalate/bind/impersonate verbs enable RBAC privilege escalation. CWE-269.",
        ),
        (
            Regex::new(r"(?i)/var/run/secrets/kubernetes\.io").unwrap(),
            "Kubernetes service account token path reference",
            Severity::Medium,
            "Direct reference to service account token path — verify this isn't being exfiltrated.",
        ),
        (
            Regex::new(r"(?i)nsenter\s+.*-t\s*(1|\$)").unwrap(),
            "nsenter container escape in K8s",
            Severity::Critical,
            "nsenter targeting PID 1 is a container escape into the host.",
        ),
    ];
}

pub struct ContainerScanner;

impl Default for ContainerScanner {
    fn default() -> Self {
        Self::new()
    }
}

impl ContainerScanner {
    pub fn new() -> Self {
        Self
    }

    fn is_container_file(path: &Path) -> ContainerFileType {
        let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
        let path_str = path.to_string_lossy().to_lowercase();

        if filename == "Dockerfile"
            || filename.starts_with("Dockerfile.")
            || filename.ends_with(".dockerfile")
        {
            return ContainerFileType::Dockerfile;
        }
        if filename == "docker-compose.yml"
            || filename == "docker-compose.yaml"
            || filename.starts_with("docker-compose.")
            || filename == "compose.yml"
            || filename == "compose.yaml"
        {
            return ContainerFileType::DockerCompose;
        }
        // Kubernetes YAML — heuristic: .yaml/.yml with k8s-related path or content
        if (path_str.ends_with(".yml") || path_str.ends_with(".yaml"))
            && (path_str.contains("k8s")
                || path_str.contains("kubernetes")
                || path_str.contains("deploy")
                || path_str.contains("manifest")
                || path_str.contains("helm")
                || path_str.contains("container"))
        {
            return ContainerFileType::Kubernetes;
        }

        ContainerFileType::NotContainer
    }

    fn is_kubernetes_content(content: &str) -> bool {
        // Detect K8s manifests by looking for apiVersion + kind
        content.contains("apiVersion:") && content.contains("kind:")
    }

    fn apply_patterns(
        path: &Path,
        content: &str,
        patterns: &[(Regex, &'static str, Severity, &'static str)],
        findings: &mut Vec<Finding>,
    ) {
        for (line_num, line) in content.lines().enumerate() {
            for (pattern, title, severity, description) in patterns.iter() {
                if pattern.is_match(line) {
                    findings.push(
                        Finding::new(
                            format!("CTR-{:03}", findings.len() + 1),
                            title.to_string(),
                            *severity,
                        )
                        .with_file(path.to_path_buf())
                        .with_line((line_num + 1) as u32)
                        .with_evidence(line.trim().to_string())
                        .with_description(description.to_string()),
                    );
                }
            }
        }
    }
}

#[derive(Debug, PartialEq)]
enum ContainerFileType {
    Dockerfile,
    DockerCompose,
    Kubernetes,
    NotContainer,
}

#[async_trait]
impl SecurityPlugin for ContainerScanner {
    fn name(&self) -> &str {
        "container"
    }

    fn version(&self) -> &str {
        "0.1.0"
    }

    fn description(&self) -> &str {
        "Detect container and Kubernetes security misconfigurations"
    }

    fn scan_phase(&self) -> ScanPhase {
        ScanPhase::All
    }

    async fn initialize(&mut self) -> Result<(), PluginError> {
        Ok(())
    }

    async fn scan(&self, context: &ScanContext<'_>) -> Result<PluginReport, PluginError> {
        let start = Instant::now();
        let mut report = PluginReport::new(self.name().to_string());

        let file_type = Self::is_container_file(context.path);

        if let Some(content) = context.file_content {
            let content_str = String::from_utf8_lossy(content);

            match file_type {
                ContainerFileType::Dockerfile => {
                    Self::apply_patterns(
                        context.path,
                        &content_str,
                        &DOCKERFILE_PATTERNS,
                        &mut report.findings,
                    );
                    report.scanned_files = 1;
                }
                ContainerFileType::DockerCompose => {
                    Self::apply_patterns(
                        context.path,
                        &content_str,
                        &COMPOSE_PATTERNS,
                        &mut report.findings,
                    );
                    report.scanned_files = 1;
                }
                ContainerFileType::Kubernetes => {
                    Self::apply_patterns(
                        context.path,
                        &content_str,
                        &K8S_PATTERNS,
                        &mut report.findings,
                    );
                    report.scanned_files = 1;
                }
                ContainerFileType::NotContainer => {
                    // Check if this is a YAML that happens to be K8s
                    let path_str = context.path.to_string_lossy();
                    if (path_str.ends_with(".yml") || path_str.ends_with(".yaml"))
                        && Self::is_kubernetes_content(&content_str)
                    {
                        Self::apply_patterns(
                            context.path,
                            &content_str,
                            &K8S_PATTERNS,
                            &mut report.findings,
                        );
                        report.scanned_files = 1;
                    }
                }
            }
        }

        report.duration_ms = start.elapsed().as_millis() as u64;
        Ok(report)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugins::traits::ScanContext;
    use std::collections::HashMap;

    #[tokio::test]
    async fn test_privileged_pod() {
        let scanner = ContainerScanner::new();
        let content = b"apiVersion: v1\nkind: Pod\nspec:\n  containers:\n  - securityContext:\n      privileged: true";
        let context = ScanContext {
            path: Path::new("k8s-pod.yaml"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(report
            .findings
            .iter()
            .any(|f| f.title.contains("privileged")));
    }

    #[tokio::test]
    async fn test_docker_socket_mount() {
        let scanner = ContainerScanner::new();
        let content = b"volumes:\n  - /var/run/docker.sock:/var/run/docker.sock";
        let context = ScanContext {
            path: Path::new("docker-compose.yml"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(report
            .findings
            .iter()
            .any(|f| f.title.contains("Docker socket")));
    }

    #[tokio::test]
    async fn test_rbac_wildcard() {
        let scanner = ContainerScanner::new();
        let content = b"apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nrules:\n- verbs: [\"*\"]\n  resources: [\"*\"]";
        let context = ScanContext {
            path: Path::new("k8s-rbac.yaml"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(report
            .findings
            .iter()
            .any(|f| f.severity == Severity::Critical));
    }

    #[tokio::test]
    async fn test_dockerfile_secret_env() {
        let scanner = ContainerScanner::new();
        let content = b"FROM ubuntu:20.04\nENV DB_PASSWORD=supersecret\nRUN apt-get update";
        let context = ScanContext {
            path: Path::new("Dockerfile"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(report
            .findings
            .iter()
            .any(|f| f.title.contains("Secret in Dockerfile")));
    }
}