diff --git a/.gitignore b/.gitignore index c85fc4a..bd92954 100644 --- a/.gitignore +++ b/.gitignore @@ -211,4 +211,4 @@ __marimo__/ docs/* rules/* tests/* -# valkyrie/* +valkyrie/repositories/* diff --git a/pyproject.toml b/pyproject.toml index 29822f4..788eec5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,13 @@ maintainers = [{ name = "#Einswilli", email = "einswilligoeh@email.com" }] requires-python = ">=3.10" keywords = [ "valkyrie", "CI/CD", "security scaner", "guardian", - "pipelines" + "pipelines", "iam", "secrets" +] +dependencies = [ + "toml>=0.10.2", + "tomli>=2.2.1", + "yamllib>=0.0.1", ] -dependencies = [] [project.optional-dependencies] dev = [ @@ -38,4 +42,4 @@ packages = ["valkyrie"] [build-system] requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" diff --git a/valkyrie/core/formatters/base.py b/valkyrie/core/formatters/base.py new file mode 100644 index 0000000..a911ddf --- /dev/null +++ b/valkyrie/core/formatters/base.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + +from valkyrie.core.types import ScanResult + +#### +## SCAN RESULT FORMATTERS BASE CLASS +##### +class ResultFormatter(ABC): + """Abstract base class for result formatting""" + + @abstractmethod + def format(self, result: ScanResult) -> str: + """Format scan result to string""" + pass \ No newline at end of file diff --git a/valkyrie/core/formatters/sarif.py b/valkyrie/core/formatters/sarif.py new file mode 100644 index 0000000..4227fe6 --- /dev/null +++ b/valkyrie/core/formatters/sarif.py @@ -0,0 +1,128 @@ +""" +Valkyrie - SARIF Scan result Foormatter +""" + +import json +from typing import List, Dict, Any +from valkyrie.core.types import ( + ScanResult, ScanStatus, SecurityFinding, + SeverityLevel +) +from .base import ResultFormatter + + +#### +## SARIF SCAN RESUT FORMATTER +##### +class SARIFFormatter(ResultFormatter): + """SARIF (Static Analysis Results Interchange Format) formatter""" + + def format(self, result: ScanResult) -> str: + """Format results as SARIF JSON""" + + sarif_report = { + "version": "2.1.0", + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "Valkyrie", + "version": "1.0.0", + "informationUri": "https://github.com/valkyrie-scanner/valkyrie", + "rules": self._generate_rules(result.findings) + } + }, + "results": self._generate_results(result.findings), + "invocations": [ + { + "executionSuccessful": result.status == ScanStatus.COMPLETED, + "startTimeUtc": result.timestamp.isoformat() + "Z", + "endTimeUtc": result.timestamp.isoformat() + "Z" + } + ] + } + ] + } + + return json.dumps(sarif_report, indent=2) + + def _generate_rules( + self, + findings: List[SecurityFinding] + ) -> List[Dict[str, Any]]: + """Generate SARIF rules from findings""" + + rules = {} + + for finding in findings: + if finding.rule_id not in rules: + rules[finding.rule_id] = { + "id": finding.rule_id, + "shortDescription": {"text": finding.title}, + "fullDescription": {"text": finding.description}, + "help": { + "text": finding.remediation_advice or "Review and fix the security issue" + }, + "properties": { + "security-severity": self._severity_to_score(finding.severity) + } + } + + return list(rules.values()) + + def _generate_results( + self, + findings: List[SecurityFinding] + ) -> List[Dict[str, Any]]: + """Generate SARIF results from findings""" + + results = [] + + for finding in findings: + result = { + "ruleId": finding.rule_id, + "message": {"text": finding.description}, + "level": self._severity_to_level(finding.severity), + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": str(finding.location.file_path) + }, + "region": { + "startLine": finding.location.line_number, + "startColumn": finding.location.column_start, + "endColumn": finding.location.column_end + } + } + } + ] + } + results.append(result) + + return results + + def _severity_to_level(self, severity: SeverityLevel) -> str: + """Convert severity to SARIF level""" + + mapping = { + SeverityLevel.CRITICAL: "error", + SeverityLevel.HIGH: "error", + SeverityLevel.MEDIUM: "warning", + SeverityLevel.LOW: "note", + SeverityLevel.INFO: "note" + } + return mapping.get(severity, "note") + + def _severity_to_score(self, severity: SeverityLevel) -> str: + """Convert severity to security score""" + + mapping = { + SeverityLevel.CRITICAL: "9.0", + SeverityLevel.HIGH: "7.0", + SeverityLevel.MEDIUM: "5.0", + SeverityLevel.LOW: "3.0", + SeverityLevel.INFO: "1.0" + } + return mapping.get(severity, "1.0") diff --git a/valkyrie/plugins/__init__.py b/valkyrie/plugins/__init__.py index 4818cd4..45df10a 100644 --- a/valkyrie/plugins/__init__.py +++ b/valkyrie/plugins/__init__.py @@ -1,4 +1,5 @@ """ +Valkyrie - Plugin module. """ from pathlib import Path from typing import List, Set, Dict, Any, Optional