File size: 5,361 Bytes
92c4ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import mimetypes
import os
from pathlib import Path
from typing import List, Optional
from atom_security.core.models import Finding, ScanResult, SecurityRule, Severity
from atom_security.core.patterns import PatternMatcher, RuleLoader


class StaticAnalyzer:
    """
    Analyzes files using regex patterns defined in signatures.yaml.
    """
    
    def __init__(self, rules_path: Optional[Path] = None):
        loader = RuleLoader(rules_path)
        self.rules = loader.load_rules()
        self.matcher = PatternMatcher(self.rules)
        
    def scan_content(self, content: str, file_type: str = 'python') -> List[Finding]:
        """Scan string content for security issues."""
        findings = []
        for rule in self.rules:
            if self._applies_to_file(rule, file_type):
                matches = self.matcher.match(content, rule)
                for line_num, line_content in matches:
                    finding = Finding(
                        rule_id=rule.id,
                        category=rule.category,
                        severity=rule.severity,
                        line_number=line_num,
                        line_content=line_content,
                        description=rule.description,
                        remediation=rule.remediation
                    )
                    findings.append(finding)
        return findings
        
    def scan_file(self, file_path: Path) -> List[Finding]:
        """Scan a single file for security issues."""
        findings = []
        
        try:
            # Determine file type
            file_type = self._detect_file_type(file_path)
            if not file_type:
                return []
                
            # Skip large files (>1MB)
            if file_path.stat().st_size > 1_000_000:
                print(f"Skipping large file: {file_path}")
                return []
                
            content = file_path.read_text(errors='ignore')
            
            # Check rules applicable to this file type
            for rule in self.rules:
                if self._applies_to_file(rule, file_type):
                    matches = self.matcher.match(content, rule)
                    
                    for line_num, line_content in matches:
                        finding = Finding(
                            rule_id=rule.id,
                            category=rule.category,
                            severity=rule.severity,
                            file_path=str(file_path),
                            line_number=line_num,
                            line_content=line_content,
                            description=rule.description,
                            remediation=rule.remediation
                        )
                        findings.append(finding)
                        
        except Exception as e:
            print(f"Error scanning {file_path}: {e}")
            
        return findings

    def scan_directory(self, directory: Path) -> ScanResult:
        """Scan a directory recursively."""
        scan_directory = Path(directory)
        all_findings = []
        files_scanned = 0
        
        import time
        start_time = time.time()
        
        # Walk directory
        for root, dirs, files in os.walk(scan_directory):
            # Skip hidden dirs
            dirs[:] = [d for d in dirs if not d.startswith('.')]
            
            for file in files:
                if file.startswith('.'):
                    continue
                    
                file_path = Path(root) / file
                findings = self.scan_file(file_path)
                all_findings.extend(findings)
                files_scanned += 1
                
        duration = time.time() - start_time
        
        # Calculate summary metrics
        max_severity = Severity.INFO
        is_safe = True
        
        severity_rank = {
            Severity.INFO: 0, 
            Severity.LOW: 1, 
            Severity.MEDIUM: 2, 
            Severity.HIGH: 3, 
            Severity.CRITICAL: 4
        }
        
        for f in all_findings:
            if severity_rank[f.severity] >= severity_rank[Severity.HIGH]:
                is_safe = False
            
            if severity_rank[f.severity] > severity_rank[max_severity]:
                max_severity = f.severity
                
        return ScanResult(
            is_safe=is_safe,
            max_severity=max_severity,
            findings=all_findings,
            scan_duration=duration,
            files_scanned=files_scanned,
            analyzers_run=["StaticAnalyzer"]
        )

    def _detect_file_type(self, file_path: Path) -> Optional[str]:
        """Map file extension to rule file_type."""
        ext = file_path.suffix.lower()
        if ext in ['.py', '.pyw']:
            return 'python'
        elif ext in ['.sh', '.bash', '.zsh']:
            return 'bash'
        elif ext in ['.md', '.markdown', '.txt']:
            return 'markdown'
        elif ext in ['.yml', '.yaml']:
            return 'manifest' # Or yaml
        # Binary check could be added here
        return None

    def _applies_to_file(self, rule: SecurityRule, file_type: str) -> bool:
        """Check if rule applies to file type."""
        return file_type in rule.file_types or 'all' in rule.file_types