refactor: add dependency vulnerability scanning and feature flags
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
- Create DependencyScanner for automated vulnerability scanning - Implement pip-audit and Bandit integration - Add vulnerability report generation with severity breakdown - Create FeatureFlagManager for gradual rollouts - Support percentage-based rollouts and user whitelisting/blacklisting - Add vulnerability threshold checking - Implement feature flag configuration persistence - Provide global manager instances for easy access
This commit is contained in:
262
aitbc/dependency_scanner.py
Normal file
262
aitbc/dependency_scanner.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Dependency vulnerability scanning utilities for AITBC
|
||||
Provides automated vulnerability scanning for Python dependencies
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
from typing import List, Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from .aitbc_logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VulnerabilityReport:
|
||||
"""Vulnerability scan report"""
|
||||
package: str
|
||||
version: str
|
||||
vulnerability_id: str
|
||||
severity: str
|
||||
description: str
|
||||
fix_available: bool
|
||||
fixed_version: Optional[str]
|
||||
|
||||
|
||||
class DependencyScanner:
|
||||
"""
|
||||
Dependency vulnerability scanner.
|
||||
Scans Python dependencies for known vulnerabilities.
|
||||
"""
|
||||
|
||||
def __init__(self, requirements_file: Optional[Path] = None):
|
||||
"""
|
||||
Initialize dependency scanner
|
||||
|
||||
Args:
|
||||
requirements_file: Path to requirements.txt or pyproject.toml
|
||||
"""
|
||||
self.requirements_file = requirements_file or Path("requirements.txt")
|
||||
self._vulnerabilities: List[VulnerabilityReport] = []
|
||||
|
||||
def scan_with_pip_audit(self) -> List[VulnerabilityReport]:
|
||||
"""
|
||||
Scan dependencies using pip-audit
|
||||
|
||||
Returns:
|
||||
List of vulnerability reports
|
||||
"""
|
||||
logger.info("Running pip-audit vulnerability scan")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pip-audit", "--format", "json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info("No vulnerabilities found")
|
||||
return []
|
||||
|
||||
# Parse JSON output
|
||||
try:
|
||||
audit_data = json.loads(result.stdout)
|
||||
return self._parse_pip_audit_output(audit_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Failed to parse pip-audit JSON output")
|
||||
return []
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.warning("pip-audit not found, skipping scan")
|
||||
return []
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("pip-audit scan timed out")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"pip-audit scan failed: {e}")
|
||||
return []
|
||||
|
||||
def scan_with_bandit(self, target_dir: Optional[Path] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Scan code for security issues using Bandit
|
||||
|
||||
Args:
|
||||
target_dir: Directory to scan (default: current directory)
|
||||
|
||||
Returns:
|
||||
List of security issues
|
||||
"""
|
||||
target_dir = target_dir or Path(".")
|
||||
logger.info(f"Running Bandit security scan on {target_dir}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bandit", "-r", str(target_dir), "-f", "json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
try:
|
||||
bandit_data = json.loads(result.stdout)
|
||||
return bandit_data.get("results", [])
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Failed to parse Bandit JSON output")
|
||||
return []
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.warning("Bandit not found, skipping scan")
|
||||
return []
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Bandit scan timed out")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Bandit scan failed: {e}")
|
||||
return []
|
||||
|
||||
def _parse_pip_audit_output(self, audit_data: Dict[str, Any]) -> List[VulnerabilityReport]:
|
||||
"""
|
||||
Parse pip-audit JSON output
|
||||
|
||||
Args:
|
||||
audit_data: Raw audit data from pip-audit
|
||||
|
||||
Returns:
|
||||
List of vulnerability reports
|
||||
"""
|
||||
vulnerabilities = []
|
||||
|
||||
for dep in audit_data.get("dependencies", []):
|
||||
for vuln in dep.get("vulnerabilities", []):
|
||||
report = VulnerabilityReport(
|
||||
package=dep.get("name", ""),
|
||||
version=dep.get("version", ""),
|
||||
vulnerability_id=vuln.get("id", ""),
|
||||
severity=vuln.get("severity", "UNKNOWN"),
|
||||
description=vuln.get("description", ""),
|
||||
fix_available=vuln.get("fix_versions", []) != [],
|
||||
fixed_version=vuln.get("fix_versions", [None])[0] if vuln.get("fix_versions") else None
|
||||
)
|
||||
vulnerabilities.append(report)
|
||||
|
||||
return vulnerabilities
|
||||
|
||||
def generate_report(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate comprehensive vulnerability report
|
||||
|
||||
Returns:
|
||||
Dictionary with scan results
|
||||
"""
|
||||
pip_audit_results = self.scan_with_pip_audit()
|
||||
bandit_results = self.scan_with_bandit()
|
||||
|
||||
# Count vulnerabilities by severity
|
||||
severity_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
|
||||
for vuln in pip_audit_results:
|
||||
severity = vuln.severity.upper()
|
||||
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"dependency_vulnerabilities": len(pip_audit_results),
|
||||
"security_issues": len(bandit_results),
|
||||
"severity_breakdown": severity_counts,
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"package": v.package,
|
||||
"version": v.version,
|
||||
"id": v.vulnerability_id,
|
||||
"severity": v.severity,
|
||||
"description": v.description,
|
||||
"fix_available": v.fix_available,
|
||||
"fixed_version": v.fixed_version
|
||||
}
|
||||
for v in pip_audit_results
|
||||
],
|
||||
"bandit_issues": bandit_results
|
||||
}
|
||||
|
||||
def save_report(self, output_file: Path) -> None:
|
||||
"""
|
||||
Save vulnerability report to file
|
||||
|
||||
Args:
|
||||
output_file: Path to output file
|
||||
"""
|
||||
report = self.generate_report()
|
||||
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
|
||||
logger.info(f"Vulnerability report saved to {output_file}")
|
||||
|
||||
|
||||
def run_dependency_scan(
|
||||
requirements_file: Optional[Path] = None,
|
||||
output_file: Optional[Path] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run comprehensive dependency vulnerability scan
|
||||
|
||||
Args:
|
||||
requirements_file: Path to requirements file
|
||||
output_file: Path to save report
|
||||
|
||||
Returns:
|
||||
Vulnerability scan report
|
||||
"""
|
||||
scanner = DependencyScanner(requirements_file)
|
||||
report = scanner.generate_report()
|
||||
|
||||
if output_file:
|
||||
scanner.save_report(output_file)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def check_vulnerability_thresholds(
|
||||
report: Dict[str, Any],
|
||||
max_critical: int = 0,
|
||||
max_high: int = 0,
|
||||
max_medium: int = 10,
|
||||
max_low: int = 50
|
||||
) -> bool:
|
||||
"""
|
||||
Check if vulnerability counts are within acceptable thresholds
|
||||
|
||||
Args:
|
||||
report: Vulnerability scan report
|
||||
max_critical: Maximum allowed critical vulnerabilities
|
||||
max_high: Maximum allowed high vulnerabilities
|
||||
max_medium: Maximum allowed medium vulnerabilities
|
||||
max_low: Maximum allowed low vulnerabilities
|
||||
|
||||
Returns:
|
||||
True if within thresholds, False otherwise
|
||||
"""
|
||||
severity = report.get("severity_breakdown", {})
|
||||
|
||||
if severity.get("CRITICAL", 0) > max_critical:
|
||||
logger.error(f"Critical vulnerabilities exceed threshold: {severity.get('CRITICAL')} > {max_critical}")
|
||||
return False
|
||||
|
||||
if severity.get("HIGH", 0) > max_high:
|
||||
logger.error(f"High vulnerabilities exceed threshold: {severity.get('HIGH')} > {max_high}")
|
||||
return False
|
||||
|
||||
if severity.get("MEDIUM", 0) > max_medium:
|
||||
logger.warning(f"Medium vulnerabilities exceed threshold: {severity.get('MEDIUM')} > {max_medium}")
|
||||
|
||||
if severity.get("LOW", 0) > max_low:
|
||||
logger.warning(f"Low vulnerabilities exceed threshold: {severity.get('LOW')} > {max_low}")
|
||||
|
||||
return True
|
||||
278
aitbc/feature_flags.py
Normal file
278
aitbc/feature_flags.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
Feature flags utilities for AITBC
|
||||
Provides feature flag management for gradual rollouts
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, Set
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
from .aitbc_logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FeatureFlag:
|
||||
"""Feature flag configuration"""
|
||||
name: str
|
||||
enabled: bool
|
||||
description: str
|
||||
rollout_percentage: float = 100.0
|
||||
whitelisted_users: Optional[Set[str]] = None
|
||||
blacklisted_users: Optional[Set[str]] = None
|
||||
enabled_since: Optional[datetime] = None
|
||||
|
||||
|
||||
class FeatureFlagManager:
|
||||
"""
|
||||
Feature flag manager for gradual rollouts.
|
||||
Provides feature flag management with user whitelisting and percentage-based rollouts.
|
||||
"""
|
||||
|
||||
def __init__(self, config_file: Optional[Path] = None):
|
||||
"""
|
||||
Initialize feature flag manager
|
||||
|
||||
Args:
|
||||
config_file: Path to feature flags configuration file
|
||||
"""
|
||||
self.config_file = config_file or Path("feature_flags.json")
|
||||
self._flags: Dict[str, FeatureFlag] = {}
|
||||
self._load_flags()
|
||||
|
||||
def _load_flags(self) -> None:
|
||||
"""Load feature flags from configuration file"""
|
||||
if not self.config_file.exists():
|
||||
logger.info(f"No feature flags file found at {self.config_file}, using defaults")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.config_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
for name, config in data.items():
|
||||
self._flags[name] = FeatureFlag(
|
||||
name=name,
|
||||
enabled=config.get("enabled", False),
|
||||
description=config.get("description", ""),
|
||||
rollout_percentage=config.get("rollout_percentage", 100.0),
|
||||
whitelisted_users=set(config.get("whitelisted_users", [])),
|
||||
blacklisted_users=set(config.get("blacklisted_users", [])),
|
||||
enabled_since=datetime.fromisoformat(config["enabled_since"]) if config.get("enabled_since") else None
|
||||
)
|
||||
|
||||
logger.info(f"Loaded {len(self._flags)} feature flags from {self.config_file}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load feature flags: {e}")
|
||||
|
||||
def save_flags(self) -> None:
|
||||
"""Save feature flags to configuration file"""
|
||||
data = {}
|
||||
for name, flag in self._flags.items():
|
||||
data[name] = {
|
||||
"enabled": flag.enabled,
|
||||
"description": flag.description,
|
||||
"rollout_percentage": flag.rollout_percentage,
|
||||
"whitelisted_users": list(flag.whitelisted_users) if flag.whitelisted_users else [],
|
||||
"blacklisted_users": list(flag.blacklisted_users) if flag.blacklisted_users else [],
|
||||
"enabled_since": flag.enabled_since.isoformat() if flag.enabled_since else None
|
||||
}
|
||||
|
||||
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.config_file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
logger.info(f"Saved {len(self._flags)} feature flags to {self.config_file}")
|
||||
|
||||
def is_enabled(
|
||||
self,
|
||||
feature_name: str,
|
||||
user_id: Optional[str] = None,
|
||||
user_hash: Optional[int] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a feature is enabled for a user
|
||||
|
||||
Args:
|
||||
feature_name: Name of the feature flag
|
||||
user_id: User identifier
|
||||
user_hash: Hash of user identifier for percentage-based rollout
|
||||
|
||||
Returns:
|
||||
True if feature is enabled, False otherwise
|
||||
"""
|
||||
flag = self._flags.get(feature_name)
|
||||
|
||||
if not flag:
|
||||
logger.warning(f"Feature flag {feature_name} not found, defaulting to disabled")
|
||||
return False
|
||||
|
||||
# Check if globally disabled
|
||||
if not flag.enabled:
|
||||
return False
|
||||
|
||||
# Check if user is blacklisted
|
||||
if flag.blacklisted_users and user_id in flag.blacklisted_users:
|
||||
return False
|
||||
|
||||
# Check if user is whitelisted
|
||||
if flag.whitelisted_users and user_id in flag.whitelisted_users:
|
||||
return True
|
||||
|
||||
# Check percentage-based rollout
|
||||
if flag.rollout_percentage < 100.0 and user_hash is not None:
|
||||
# Use modulo to determine if user is in rollout percentage
|
||||
if (user_hash % 100) < flag.rollout_percentage:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Feature is globally enabled
|
||||
return True
|
||||
|
||||
def enable_feature(self, feature_name: str, rollout_percentage: float = 100.0) -> None:
|
||||
"""
|
||||
Enable a feature flag
|
||||
|
||||
Args:
|
||||
feature_name: Name of the feature flag
|
||||
rollout_percentage: Rollout percentage (0-100)
|
||||
"""
|
||||
if feature_name not in self._flags:
|
||||
self._flags[feature_name] = FeatureFlag(
|
||||
name=feature_name,
|
||||
enabled=True,
|
||||
description="",
|
||||
rollout_percentage=rollout_percentage,
|
||||
enabled_since=datetime.now()
|
||||
)
|
||||
else:
|
||||
self._flags[feature_name].enabled = True
|
||||
self._flags[feature_name].rollout_percentage = rollout_percentage
|
||||
if not self._flags[feature_name].enabled_since:
|
||||
self._flags[feature_name].enabled_since = datetime.now()
|
||||
|
||||
logger.info(f"Enabled feature flag {feature_name} with {rollout_percentage}% rollout")
|
||||
self.save_flags()
|
||||
|
||||
def disable_feature(self, feature_name: str) -> None:
|
||||
"""
|
||||
Disable a feature flag
|
||||
|
||||
Args:
|
||||
feature_name: Name of the feature flag
|
||||
"""
|
||||
if feature_name in self._flags:
|
||||
self._flags[feature_name].enabled = False
|
||||
logger.info(f"Disabled feature flag {feature_name}")
|
||||
self.save_flags()
|
||||
|
||||
def add_whitelisted_user(self, feature_name: str, user_id: str) -> None:
|
||||
"""
|
||||
Add user to feature whitelist
|
||||
|
||||
Args:
|
||||
feature_name: Name of the feature flag
|
||||
user_id: User identifier
|
||||
"""
|
||||
if feature_name not in self._flags:
|
||||
self._flags[feature_name] = FeatureFlag(
|
||||
name=feature_name,
|
||||
enabled=False,
|
||||
description="",
|
||||
whitelisted_users=set()
|
||||
)
|
||||
|
||||
if not self._flags[feature_name].whitelisted_users:
|
||||
self._flags[feature_name].whitelisted_users = set()
|
||||
|
||||
self._flags[feature_name].whitelisted_users.add(user_id)
|
||||
logger.info(f"Added {user_id} to whitelist for {feature_name}")
|
||||
self.save_flags()
|
||||
|
||||
def add_blacklisted_user(self, feature_name: str, user_id: str) -> None:
|
||||
"""
|
||||
Add user to feature blacklist
|
||||
|
||||
Args:
|
||||
feature_name: Name of the feature flag
|
||||
user_id: User identifier
|
||||
"""
|
||||
if feature_name not in self._flags:
|
||||
self._flags[feature_name] = FeatureFlag(
|
||||
name=feature_name,
|
||||
enabled=False,
|
||||
description="",
|
||||
blacklisted_users=set()
|
||||
)
|
||||
|
||||
if not self._flags[feature_name].blacklisted_users:
|
||||
self._flags[feature_name].blacklisted_users = set()
|
||||
|
||||
self._flags[feature_name].blacklisted_users.add(user_id)
|
||||
logger.info(f"Added {user_id} to blacklist for {feature_name}")
|
||||
self.save_flags()
|
||||
|
||||
def get_all_flags(self) -> Dict[str, FeatureFlag]:
|
||||
"""
|
||||
Get all feature flags
|
||||
|
||||
Returns:
|
||||
Dictionary of all feature flags
|
||||
"""
|
||||
return self._flags.copy()
|
||||
|
||||
def get_flag_status(self, feature_name: str) -> Optional[FeatureFlag]:
|
||||
"""
|
||||
Get status of a specific feature flag
|
||||
|
||||
Args:
|
||||
feature_name: Name of the feature flag
|
||||
|
||||
Returns:
|
||||
Feature flag or None if not found
|
||||
"""
|
||||
return self._flags.get(feature_name)
|
||||
|
||||
|
||||
# Global feature flag manager instance
|
||||
_global_feature_flag_manager: Optional[FeatureFlagManager] = None
|
||||
|
||||
|
||||
def get_feature_flag_manager(config_file: Optional[Path] = None) -> FeatureFlagManager:
|
||||
"""
|
||||
Get the global feature flag manager instance
|
||||
|
||||
Args:
|
||||
config_file: Path to feature flags configuration file
|
||||
|
||||
Returns:
|
||||
FeatureFlagManager instance
|
||||
"""
|
||||
global _global_feature_flag_manager
|
||||
if _global_feature_flag_manager is None:
|
||||
_global_feature_flag_manager = FeatureFlagManager(config_file)
|
||||
return _global_feature_flag_manager
|
||||
|
||||
|
||||
def is_feature_enabled(
|
||||
feature_name: str,
|
||||
user_id: Optional[str] = None,
|
||||
user_hash: Optional[int] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a feature is enabled using global manager
|
||||
|
||||
Args:
|
||||
feature_name: Name of the feature flag
|
||||
user_id: User identifier
|
||||
user_hash: Hash of user identifier
|
||||
|
||||
Returns:
|
||||
True if feature is enabled, False otherwise
|
||||
"""
|
||||
manager = get_feature_flag_manager()
|
||||
return manager.is_enabled(feature_name, user_id, user_hash)
|
||||
Reference in New Issue
Block a user