diff --git a/valkyrie/plugins/iamx/__init__.py b/valkyrie/plugins/iamx/__init__.py new file mode 100644 index 0000000..b1a34db --- /dev/null +++ b/valkyrie/plugins/iamx/__init__.py @@ -0,0 +1,139 @@ +import hashlib +from pathlib import Path +from typing import Dict, List, Any + +from valkyrie.plugins import BaseSecurityRule +from valkyrie.core.types import ( + ScanRule, ScannerPlugin, RuleMetadata, SecurityFinding, + FileLocation, SeverityLevel, FindingCategory, +) + +from .conf import RISKY_PATTERNS + + +#### +## IAM CONFIGURATION RULE +##### +class IAMConfigurationRule(BaseSecurityRule): + """Rule for detecting risky IAM configurations""" + + def __init__(self): + metadata = RuleMetadata( + id = "iam-001", + name = "IAM Configuration Scanner", + description = "Detects overly permissive IAM policies and configurations", + category = FindingCategory.IAM_CONFIG, + severity = SeverityLevel.HIGH, + author = "Valkyrie Core Team", + tags = {"iam", "aws", "gcp", "azure", "permissions"} + ) + super().__init__(metadata) + + # Define risky patterns + self.risky_patterns = RISKY_PATTERNS + + def is_applicable(self, file_path: Path) -> bool: + """Check if file contains IAM configurations""" + + iam_files = { + '.json', '.yaml', '.yml', '.tf', '.hcl' + } + + if file_path.suffix.lower() not in iam_files: + return False + + # Check filename patterns + iam_patterns = [ + 'policy', 'iam', 'role', 'permission', 'access', + 'cloudformation', 'terraform', 'main.tf' + ] + + filename_lower = file_path.name.lower() + return any( + pattern in filename_lower + for pattern in iam_patterns + ) + + async def scan( + self, + file_path: Path, + content: str + ) -> List[SecurityFinding]: + """Scan IAM configuration files""" + + findings = [] + lines = content.split('\n') + + for line_num, line in enumerate(lines, 1): + for pattern_info in self.risky_patterns: + matches = pattern_info.pattern.finditer(line) + + for match in matches: + finding = SecurityFinding( + id = hashlib.md5(f"{file_path}:{line_num}:{pattern_info.name}".encode()).hexdigest(), + title = f"Risky IAM Configuration: {pattern_info.name}", + description = pattern_info.description, + severity = pattern_info.severity, + category = self.metadata.category, + location = FileLocation( + file_path = file_path, + line_number = line_num, + column_start = match.start(), + column_end = match.end() + ), + rule_id = self.metadata.id, + confidence = 0.8, + metadata={ + "pattern_name": pattern_info["name"], + "line_content": line.strip(), + "cloud_provider": self._detect_cloud_provider(content) + }, + remediation_advice = ( + "Apply principle of least privilege. " + "Specify exact resources and actions needed." + ) + ) + findings.append(finding) + + return findings + + def _detect_cloud_provider(self, content: str) -> str: + """Detect cloud provider from content""" + + content_lower = content.lower() + + if 'amazonaws.com' in content_lower or 'aws:' in content_lower: + return "AWS" + elif 'googleapis.com' in content_lower or 'gcp' in content_lower: + return "GCP" + elif 'azure' in content_lower or 'microsoft.com' in content_lower: + return "Azure" + else: + return "Unknown" + + +#### +## IAM CONFIGURATION PLUGIN +##### +class IAMPlugin(ScannerPlugin): + """Plugin for IAM configuration scanning""" + + @property + def name(self) -> str: + return "iam-scanner" + + @property + def version(self) -> str: + return "0.1.0" + + async def initialize(self, config: Dict[str, Any]) -> None: + """Initialize IAM plugin""" + pass + + async def get_rules(self) -> List[ScanRule]: + """Return IAM scanning rules""" + return [IAMConfigurationRule()] + + async def cleanup(self) -> None: + """Cleanup plugin resources""" + pass diff --git a/valkyrie/plugins/iamx/conf.py b/valkyrie/plugins/iamx/conf.py new file mode 100644 index 0000000..433ad50 --- /dev/null +++ b/valkyrie/plugins/iamx/conf.py @@ -0,0 +1,62 @@ +import re +from dataclasses import dataclass +from typing import ( + List, Pattern, Optional +) + +from valkyrie.core.types import SeverityLevel + + +#### +## SECRET PATTERN MODEL +##### +@dataclass +class RiskyPattern: + """Pattern definition for secret detection""" + + name: str + pattern: Pattern[str] + description: Optional[str] = None + severity: SeverityLevel = SeverityLevel.MEDIUM + + +#### +RISKY_PATTERNS: List[RiskyPattern] = [ + ## AMAZON WEB SERVICES (AWS) + RiskyPattern( + **{ + "name": "AWS Wildcard Resource", + "pattern": re.compile(r'"Resource"\s*:\s*"\*"', re.IGNORECASE), + "description": "Policy allows access to all resources", + "severity": SeverityLevel.CRITICAL + }, + ), + RiskyPattern( + **{ + "name": "AWS Admin Access", + "pattern": re.compile(r'"Action"\s*:\s*"\*"', re.IGNORECASE), + "description": "Policy grants all actions (admin access)", + "severity": SeverityLevel.CRITICAL + }, + ), + + ## GOOGLE CLOUD (GCP) + RiskyPattern( + **{ + "name": "GCP All Scopes", + "pattern": re.compile(r'https://www\.googleapis\.com/auth/cloud-platform', re.IGNORECASE), + "description": "Grants access to all Google Cloud Platform services", + "severity": SeverityLevel.HIGH + }, + ), + + ## MICROSOFT AZURE + RiskyPattern( + **{ + "name": "Azure Contributor Role", + "pattern": re.compile(r'"roleDefinitionId".*"b24988ac-6180-42a0-ab88-20f7382dd24c"', re.IGNORECASE), + "description": "Grants broad contributor access to Azure resources", + "severity": SeverityLevel.MEDIUM + } + ) +] diff --git a/valkyrie/plugins/secrets/__init__.py b/valkyrie/plugins/secrets/__init__.py index 0fd003f..fb9e45b 100644 --- a/valkyrie/plugins/secrets/__init__.py +++ b/valkyrie/plugins/secrets/__init__.py @@ -14,7 +14,7 @@ FileLocation ) -from valkyrie.plugins.secrets.conf import ( +from .conf import ( SECRETS_PATTERNS, SecretPattern ) @@ -82,11 +82,13 @@ async def scan(self, file_path: Path, content: str) -> List[SecurityFinding]: for line_num, line in enumerate(lines, 1): # Skip comments and obvious false positives line_lower = line.lower().strip() - if (line_lower.startswith('#') or + if ( + line_lower.startswith('#') or line_lower.startswith('//') or 'example' in line_lower or 'placeholder' in line_lower or - 'your_api_key_here' in line_lower): + 'your_api_key_here' in line_lower + ): continue for pattern in self.patterns: diff --git a/valkyrie/plugins/vulnera/__init__.py b/valkyrie/plugins/vulnera/__init__.py index 354a67d..f4619c3 100644 --- a/valkyrie/plugins/vulnera/__init__.py +++ b/valkyrie/plugins/vulnera/__init__.py @@ -127,7 +127,7 @@ def name(self) -> str: @property def version(self) -> str: - return "1.0.0" + return "0.1.0" async def initialize(self, config: Dict[str, Any]) -> None: """Initialize plugin and load vulnerability database""" diff --git a/valkyrie/plugins/vulnera/parser.py b/valkyrie/plugins/vulnera/parser.py index 9d1621d..0a1b22d 100644 --- a/valkyrie/plugins/vulnera/parser.py +++ b/valkyrie/plugins/vulnera/parser.py @@ -189,7 +189,7 @@ def parse(self) -> List[Dependency]: ##### DependencyParser.register() class PackageLockParser(BaseDependencyParser): - """Parser foor package-lock.json (Node.js)""" + """Parser for package-lock.json (Node.js)""" @property def dep_file(self): @@ -596,7 +596,7 @@ def parse(self) -> List[Dependency]: ##### DependencyParser.register() class ComposerLockParser(BaseDependencyParser): - """Parser pour composer.lock (PHP)""" + """Parser for composer.lock (PHP)""" @property def dep_file(self):