Skip to content

Commit f405674

Browse files
committed
valkyrie,plugins: add vulnera vulnerablity scanner plugin
1 parent d716401 commit f405674

File tree

4 files changed

+897
-3
lines changed

4 files changed

+897
-3
lines changed

valkyrie/plugins/__init__.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
"""
33
from pathlib import Path
4-
from typing import List, Set, Dict, Any
4+
from typing import List, Set, Dict, Any, Optional
5+
import logging
56

67
from valkyrie.core.types import (
78
RuleMetadata, SecurityFinding, ScanRule,
@@ -15,8 +16,13 @@
1516
class BaseSecurityRule(ScanRule):
1617
"""Base implementation for security rules"""
1718

18-
def __init__(self, metadata: RuleMetadata):
19+
def __init__(
20+
self,
21+
metadata: RuleMetadata,
22+
logger: Optional[logging.Logger] = None
23+
):
1924
self._metadata = metadata
25+
self.logger = logger or logging.getLogger(__name__)
2026

2127
@property
2228
def metadata(self) -> RuleMetadata:
@@ -42,9 +48,13 @@ async def scan(
4248
class PluginManager:
4349
"""Manages scanner plugins and their lifecycle"""
4450

45-
def __init__(self):
51+
def __init__(
52+
self,
53+
logger: Optional[logging.Logger] = None
54+
):
4655
self.plugins: Dict[str, ScannerPlugin] = {}
4756
self.enabled_plugins: Set[str] = set()
57+
self.logger = logger or logging.getLogger(__name__)
4858

4959
async def register_plugin(
5060
self,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import hashlib
2+
from pathlib import Path
3+
from typing import Dict, List, Any
4+
5+
from valkyrie.plugins import BaseSecurityRule
6+
from valkyrie.core.types import (
7+
ScanRule, ScannerPlugin, RuleMetadata, SecurityFinding,
8+
FileLocation, SeverityLevel, FindingCategory,
9+
)
10+
11+
from .conf import VulnerabilityInfo
12+
from .parser import parse_dependencies, is_supported
13+
14+
15+
####
16+
## VULNERABILITY RULE
17+
#####
18+
class DependencyVulnerabilityRule(BaseSecurityRule):
19+
"""Rule for detecting vulnerable dependencies"""
20+
21+
def __init__(
22+
self,
23+
vulnerability_db: Dict[str, List[VulnerabilityInfo]]
24+
):
25+
metadata = RuleMetadata(
26+
id = "deps-001",
27+
name = "Dependency Vulnerability Scanner",
28+
description = "Scans dependencies for known vulnerabilities",
29+
category = FindingCategory.DEPENDENCIES,
30+
severity = SeverityLevel.HIGH,
31+
author = "Valkyrie Core Team",
32+
tags = {"dependencies", "vulnerabilities", "sbom"}
33+
)
34+
super().__init__(metadata)
35+
self.vulnerability_db = vulnerability_db
36+
37+
def is_applicable(self, file_path: Path) -> bool:
38+
"""Check if file is a supported dependency manifest"""
39+
40+
return is_supported(file_path)
41+
42+
async def scan(
43+
self,
44+
file_path: Path,
45+
content: str
46+
) -> List[SecurityFinding]:
47+
"""Scan dependency file for vulnerabilities."""
48+
49+
findings = []
50+
51+
try:
52+
dependencies = await self._parse_dependencies(file_path)
53+
54+
for dep_name, version in dependencies.items():
55+
if dep_name in self.vulnerability_db:
56+
vulnerabilities = self.vulnerability_db[dep_name]
57+
58+
for vuln in vulnerabilities:
59+
if self._is_version_affected(version, vuln.affected_versions):
60+
61+
# Then add it to findings
62+
finding = SecurityFinding(
63+
id = hashlib.md5(f"{file_path}:{dep_name}:{vuln.cve_id}".encode()).hexdigest(),
64+
title = f"Vulnerable dependency: {dep_name}",
65+
description = f"Dependency {dep_name}@{version} has vulnerability {vuln.cve_id}: {vuln.description}",
66+
severity = vuln.severity,
67+
category = self.metadata.category,
68+
location = FileLocation(file_path=file_path, line_number=1),
69+
rule_id = self.metadata.id,
70+
confidence = 0.9,
71+
metadata = {
72+
"dependency": dep_name,
73+
"version": version,
74+
"cve_id": vuln.cve_id,
75+
"fixed_versions": vuln.fixed_versions,
76+
"references": vuln.references
77+
},
78+
remediation_advice = (
79+
f"Update {dep_name} to version "
80+
f"{', '.join(vuln.fixed_versions) if vuln.fixed_versions else 'latest'}"
81+
)
82+
)
83+
findings.append(finding)
84+
85+
except Exception as e:
86+
# Log parsing error but don't fail the scan
87+
self.logger.warning(
88+
f'Error scanning file {file_path}: {e}'
89+
)
90+
91+
return findings
92+
93+
async def _parse_dependencies(self, file_path: Path) -> Dict[str, str]:
94+
"""Parse dependencies from file content"""
95+
96+
dependencies = {}
97+
98+
# Parse the dependency file
99+
for dep in parse_dependencies(file_path=file_path):
100+
dependencies[dep.name] = dep.version
101+
102+
return dependencies
103+
104+
def _is_version_affected(
105+
self,
106+
version: str,
107+
affected_versions: List[str]
108+
) -> bool:
109+
"""Check if version is affected by vulnerability"""
110+
# Simplified version comparison for now
111+
# I'll use a proper semver library in next push
112+
return version in affected_versions
113+
114+
115+
####
116+
## VULNERABILITY PLUGIN
117+
#####
118+
class DependenciesPlugin(ScannerPlugin):
119+
"""Plugin for dependency vulnerability scanning"""
120+
121+
def __init__(self):
122+
self.vulnerability_db: Dict[str, List[VulnerabilityInfo]] = {}
123+
124+
@property
125+
def name(self) -> str:
126+
return "vulnera"
127+
128+
@property
129+
def version(self) -> str:
130+
return "1.0.0"
131+
132+
async def initialize(self, config: Dict[str, Any]) -> None:
133+
"""Initialize plugin and load vulnerability database"""
134+
await self._load_vulnerability_db()
135+
136+
async def _load_vulnerability_db(self) -> None:
137+
"""Load vulnerability database from external sources"""
138+
139+
# Normally we need to call an external service
140+
# Or load a local vulnerabilities dbm
141+
# but i'm usinng a mock database and
142+
# i'll fix that in next push
143+
self.vulnerability_db = {
144+
"lodash": [
145+
VulnerabilityInfo(
146+
cve_id="CVE-2021-23337",
147+
severity=SeverityLevel.HIGH,
148+
description="Prototype pollution in lodash",
149+
affected_versions=["4.17.20"],
150+
fixed_versions=["4.17.21"],
151+
references=["https://nvd.nist.gov/vuln/detail/CVE-2021-23337"]
152+
)
153+
],
154+
"requests": [
155+
VulnerabilityInfo(
156+
cve_id="CVE-2023-32681",
157+
severity=SeverityLevel.MEDIUM,
158+
description="Certificate verification bypass in requests",
159+
affected_versions=["2.30.0", "2.29.0"],
160+
fixed_versions=["2.31.0"],
161+
references=["https://nvd.nist.gov/vuln/detail/CVE-2023-32681"]
162+
)
163+
]
164+
}
165+
166+
async def get_rules(self) -> List[ScanRule]:
167+
"""Return dependency scanning rules"""
168+
return [DependencyVulnerabilityRule(self.vulnerability_db)]
169+
170+
async def cleanup(self) -> None:
171+
"""Cleanup plugin resources"""
172+
pass

valkyrie/plugins/vulnera/conf.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import List, Optional
2+
from dataclasses import dataclass, field
3+
from valkyrie.core.types import (
4+
SeverityLevel
5+
)
6+
7+
8+
####
9+
## VULNERABILITY MODEL
10+
#####
11+
@dataclass
12+
class VulnerabilityInfo:
13+
"""Information about a vulnerability"""
14+
15+
cve_id: str
16+
severity: SeverityLevel
17+
description: str
18+
affected_versions: List[str]
19+
fixed_versions: List[str] = field(default_factory=list)
20+
references: List[str] = field(default_factory=list)
21+
22+
23+
####
24+
## DEPENDENCY MODEL
25+
#####
26+
@dataclass
27+
class Dependency:
28+
"""Project dependency rpresentation model"""
29+
30+
name: str
31+
version: Optional[str] = None
32+
dev: bool = False
33+
source: Optional[str] = None
34+
35+
def __str__(self):
36+
version_str = f"@{self.version}" if self.version else ""
37+
dev_str = " (dev)" if self.dev else ""
38+
return f"{self.name}{version_str}{dev_str}"
39+
40+
41+
#### DEPENDENCIES
42+
DEPS_FIES = {
43+
# Node.js
44+
'package.json', 'package-lock.json', 'yarn.lock',
45+
46+
# Python
47+
'requirements.txt', 'Pipfile', 'Pipfile.lock', 'poetry.lock',
48+
49+
# Java
50+
'pom.xml', 'gradle.build',
51+
52+
# Rust
53+
'Cargo.toml', 'Cargo.lock',
54+
55+
# Go
56+
'go.mod', 'go.sum',
57+
58+
# PHP
59+
'composer.json', 'composer.lock'
60+
}

0 commit comments

Comments
 (0)