diff --git a/PROJECTS/advanced/ai-threat-detection/backend/alembic.ini b/PROJECTS/advanced/ai-threat-detection/backend/alembic.ini index d89eff32..10cf41db 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/alembic.ini +++ b/PROJECTS/advanced/ai-threat-detection/backend/alembic.ini @@ -1,5 +1,15 @@ # ©AngelaMos | 2026 # alembic.ini +# +# Alembic database migration configuration +# +# Points script_location to the alembic/ directory and sets +# the default asyncpg connection URL (overridden at runtime +# by env.py from settings). Configures Python logging with +# WARN level for root and sqlalchemy.engine, INFO for +# alembic, all routed to a stderr console handler with +# generic format. Connects to alembic/env.py, +# alembic/versions/, app/config [alembic] script_location = alembic diff --git a/PROJECTS/advanced/ai-threat-detection/backend/alembic/env.py b/PROJECTS/advanced/ai-threat-detection/backend/alembic/env.py index a1ad240b..cb3caf4f 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/alembic/env.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/alembic/env.py @@ -1,6 +1,21 @@ """ ©AngelaMos | 2026 env.py + +Alembic migration environment with async PostgreSQL engine +support + +Configures SQLModel.metadata as the target for autogenerate, +imports model registrations (ModelMetadata, ThreatEvent) to +ensure table definitions are available. run_migrations_ +offline generates SQL scripts without a connection. run_ +migrations_online creates an async engine with NullPool and +executes migrations via run_sync. Mode is selected based on +context.is_offline_mode() + +Connects to: + app/config - settings.database_url + app/models - SQLModel table registrations """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/__main__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/__main__.py index 8c14c2fb..fea9ea9c 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/__main__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/__main__.py @@ -1,6 +1,15 @@ """ ©AngelaMos | 2026 __main__.py + +Uvicorn entry point for the AngelusVigil API server + +Launches app.main:app via uvicorn using host, port, and +reload settings from app.config.settings + +Connects to: + config.py - settings.host, settings.port, settings.debug + main.py - ASGI application instance """ import uvicorn diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/__init__.py index e1add2a9..db76f7be 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +API package containing FastAPI route modules for health, +ingest, threats, stats, models, and websocket endpoints """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/deps.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/deps.py index ec1b6674..7858aa38 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/deps.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/deps.py @@ -1,6 +1,20 @@ """ ©AngelaMos | 2026 deps.py + +FastAPI dependency injection providers for API key +authentication and async database sessions + +require_api_key checks the X-API-Key header against +settings.api_key, returning 401 if mismatched (no-op +when api_key is unconfigured). get_session yields an +AsyncSession from the app-level session_factory stored +on app.state during lifespan initialization + +Connects to: + config.py - settings.api_key + factory.py - app.state.session_factory + api/ - injected via Depends() in route handlers """ from collections.abc import AsyncIterator diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/health.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/health.py index d333162a..d4085edd 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/health.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/health.py @@ -1,6 +1,21 @@ """ ©AngelaMos | 2026 health.py + +Health and readiness probe endpoints for container +orchestration + +GET /health returns liveness status with uptime_seconds +and pipeline_running flag. GET /ready checks database +connectivity (SELECT 1) and Redis ping, reports +models_loaded status, and returns 503 if any dependency +is down. Both endpoints read from app.state set during +lifespan + +Connects to: + factory.py - app.state.startup_time, + pipeline_running, db_engine + core/redis_manager - redis_manager.ping() """ import time diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/ingest.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/ingest.py index 283d2ab0..fbaf3677 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/ingest.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/ingest.py @@ -1,6 +1,21 @@ """ ©AngelaMos | 2026 ingest.py + +Batch log ingestion endpoint for pushing raw log lines +into the detection pipeline + +POST /ingest/batch accepts a BatchIngestRequest (list of +raw log line strings), pushes each into the pipeline's +raw_queue via put_nowait, stops on QueueFull, and returns +the count of successfully queued lines. Protected by +require_api_key dependency + +Connects to: + deps.py - require_api_key + core/ingestion/ + pipeline.py - pipeline.raw_queue + factory.py - app.state.pipeline """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/models_api.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/models_api.py index 33c9b539..4730f207 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/models_api.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/models_api.py @@ -1,6 +1,28 @@ """ ©AngelaMos | 2026 models_api.py + +ML model status and retraining endpoints + +GET /models/status returns models_loaded flag, detection +_mode (hybrid or rules), and active model metadata from +the database. POST /models/retrain dispatches a +background retraining job that loads stored ThreatEvents, +labels them using review_label or score thresholds +(SCORE_ATTACK_THRESHOLD 0.5, SCORE_NORMAL_CEILING 0.3), +supplements with synthetic data if below MIN_TRAINING_ +SAMPLES (200), runs TrainingOrchestrator, and writes +model metadata. _fallback_synthetic spawns a subprocess +CLI train command when no real events exist + +Connects to: + config.py - settings.model_dir, ensemble + weights + models/model_metadata - ModelMetadata queries + models/threat_event - ThreatEvent training data + ml/orchestrator - TrainingOrchestrator + ml/synthetic - generate_mixed_dataset + cli/main - _write_metadata """ import logging diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/stats.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/stats.py index 8770c5e5..3f8fb612 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/stats.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/stats.py @@ -1,6 +1,19 @@ """ ©AngelaMos | 2026 stats.py + +Threat statistics endpoint returning aggregated metrics +for a configurable time window + +GET /stats accepts a range query parameter (default +"24h") and delegates to stats_service.get_stats for +database aggregation, returning a StatsResponse + +Connects to: + deps.py - get_session dependency + schemas/stats - StatsResponse model + services/stats_ + service - get_stats business logic """ from fastapi import APIRouter, Depends, Query diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/threats.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/threats.py index 9b8f2f69..64bac35c 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/threats.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/threats.py @@ -1,6 +1,23 @@ """ ©AngelaMos | 2026 threats.py + +Threat event CRUD endpoints with filtering and +pagination + +GET /threats lists events with optional severity, +source_ip, since/until datetime filters, and limit/ +offset pagination (max 100). GET /threats/{threat_id} +fetches a single event by UUID, returning 404 if not +found. Both delegate to threat_service for database +queries + +Connects to: + deps.py - get_session dependency + schemas/threats - ThreatEventResponse, + ThreatListResponse + services/threat_ + service - get_threats, get_threat_by_id """ import uuid diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/api/websocket.py b/PROJECTS/advanced/ai-threat-detection/backend/app/api/websocket.py index 60fe4c2c..44273529 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/api/websocket.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/api/websocket.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 websocket.py + +WebSocket endpoint streaming real-time threat alerts via +Redis pub/sub relay + +WS /ws/alerts accepts a client connection, subscribes to +the ALERTS_CHANNEL via a per-client Redis pubsub instance, +and runs two concurrent tasks: _relay forwards published +messages as WebSocket text frames, _receive drains client +messages until disconnect. asyncio.wait with FIRST_ +COMPLETED cancels the other task on disconnect, then +unsubscribes and closes the pubsub. Per-client subscribers +ensure correct multi-worker behavior + +Connects to: + core/alerts - ALERTS_CHANNEL constant + core/redis_manager- redis_manager.client """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/config.py b/PROJECTS/advanced/ai-threat-detection/backend/app/config.py index df54f9da..93e4c49d 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/config.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/config.py @@ -1,6 +1,27 @@ """ ©AngelaMos | 2026 config.py + +Pydantic-settings application configuration loaded from +environment variables and .env file + +Defines the Settings model with defaults for: server +(host 0.0.0.0, port 8000, debug, log_level), database +(postgresql+asyncpg URL), Redis URL, GeoIP MaxMind +database path, nginx log path, pipeline queue sizes +(raw 1000, parsed 500, feature 200, alert 100), batch +settings (size 32, timeout 50ms), and ML configuration +(model_dir, detection_mode, ensemble weights for +autoencoder/random-forest/isolation-forest at 0.40/0.40 +/0.20, ae_threshold_percentile 99.5, MLflow tracking +URI). Exports a module-level singleton settings instance + +Connects to: + factory.py - consumed in lifespan and create_app + __main__.py - server host/port/reload + core/ingestion/ - queue sizes, log path + core/detection/ - model_dir, ensemble weights + core/enrichment/ - geoip_db_path """ from pydantic_settings import BaseSettings, SettingsConfigDict diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/__init__.py index e1add2a9..ec199995 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +Core package containing detection, ingestion, feature +engineering, enrichment, and alert subsystems """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/__init__.py index 84399ce3..eadfbffc 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/__init__.py @@ -1,6 +1,9 @@ """ ©AngelaMos | 2026 __init__.py + +Alerts package defining the ALERTS_CHANNEL constant for +Redis pub/sub real-time threat notification """ ALERTS_CHANNEL = "alerts" diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/dispatcher.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/dispatcher.py index 27d10f28..c2c67351 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/dispatcher.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/alerts/dispatcher.py @@ -1,6 +1,26 @@ """ ©AngelaMos | 2026 dispatcher.py + +Alert dispatcher routing scored threat events to storage, +Redis pub/sub, and structured logging + +AlertDispatcher.dispatch receives a ScoredRequest from the +pipeline, classifies severity via classify_severity, logs +every event, and for MEDIUM+ severity persists to +PostgreSQL via create_threat_event and publishes a +WebSocketAlert JSON payload to the ALERTS_CHANNEL for +real-time WebSocket relay + +Connects to: + core/alerts/__init__ - ALERTS_CHANNEL + core/detection/ + ensemble - classify_severity + core/ingestion/ + pipeline - ScoredRequest dataclass + schemas/websocket - WebSocketAlert model + services/threat_ + service - create_threat_event """ import logging diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/__init__.py index e1add2a9..c2c47817 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +Detection package containing the rule engine, ONNX +inference engine, and ensemble scoring utilities """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/ensemble.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/ensemble.py index 7ee1c7ae..85046b4a 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/ensemble.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/ensemble.py @@ -1,6 +1,27 @@ """ ©AngelaMos | 2026 ensemble.py + +Score normalization, fusion, and severity classification +utilities for the ML ensemble + +normalize_ae_score maps autoencoder reconstruction error +to [0,1] using 2x threshold scaling. normalize_if_score +inverts sklearn isolation forest scores to [0,1]. +fuse_scores computes a weighted average across available +model scores. blend_scores combines ML ensemble and rule +engine scores with configurable ml_weight (default 0.7). +classify_severity maps unified score to HIGH (>=0.7), +MEDIUM (>=0.5), or LOW + +Connects to: + core/detection/ + inference - raw model scores passed to normalizers + core/detection/ + rules - classify_severity used for rule results + core/ingestion/ + pipeline - fuse_scores and blend_scores in + scoring stage """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/inference.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/inference.py index 2aee512c..2111795b 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/inference.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/inference.py @@ -1,6 +1,27 @@ """ ©AngelaMos | 2026 inference.py + +ONNX-based inference engine for the 3-model ML ensemble + +InferenceEngine loads autoencoder (ae.onnx), random +forest (rf.onnx), and isolation forest (if.onnx) sessions +plus RobustScaler parameters (scaler.json) and anomaly +threshold (threshold.json) from a model directory. predict +runs all 3 models on a batch of feature vectors: applies +_scale_for_ae to normalize autoencoder input, computes +reconstruction MSE for ae scores, extracts attack +probability from skl2onnx RF output format via _extract_ +rf_proba, and returns raw IF decision scores. Returns +None when models are unavailable. Each ONNX session uses +single-threaded execution (inter/intra_op_num_threads=1) + +Connects to: + config.py - settings.model_dir + factory.py - _load_inference_engine at startup + core/ingestion/ + pipeline - batch inference in scoring stage + ml/export_onnx - produces the ONNX model files """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/rules.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/rules.py index 7b56f605..9a43184b 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/rules.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/detection/rules.py @@ -1,6 +1,32 @@ """ ©AngelaMos | 2026 rules.py + +Cold-start rule-based detection engine inspired by +ModSecurity Core Rule Set + +RuleEngine.score_request evaluates requests against 7 +regex-based _PatternRules (LOG4SHELL 0.95, COMMAND_ +INJECTION 0.90, SQL_INJECTION 0.85, XSS 0.80, FILE_ +INCLUSION 0.75, SSRF 0.70, PATH_TRAVERSAL 0.60), +double-encoding detection (0.40), scanner user-agent +signature matching (0.35), and 2 _ThresholdRules +(RATE_ANOMALY >100 req/min 0.30, HIGH_ERROR_RATE >50% +0.25). Final score takes the highest match plus 0.05 +boost per additional rule, capped at 1.0. Returns a +RuleResult with threat_score, severity, matched_rules, +and component_scores + +Connects to: + core/features/ + patterns - compiled regex patterns (SQLI, + XSS, LOG4SHELL, etc.) + core/features/ + signatures - SCANNER_USER_AGENTS list + core/detection/ + ensemble - classify_severity + core/ingestion/ + parsers - ParsedLogEntry """ import re diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/__init__.py index e1add2a9..5db31840 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +Enrichment package providing GeoIP lookup services for +IP-to-location resolution """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/geoip.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/geoip.py index 96328ae1..44a9cf95 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/geoip.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/enrichment/geoip.py @@ -1,6 +1,24 @@ """ ©AngelaMos | 2026 geoip.py + +Async GeoIP lookup service backed by MaxMind GeoLite2-City +database + +GeoIPService loads a .mmdb reader on init, returning None +for missing databases. lookup resolves an IP to a GeoResult +(country ISO code, city, lat, lon), skipping private/ +loopback addresses and unknown entries. swap_reader +atomically replaces the database reader for hot-reload +after .mmdb updates. All blocking geoip2 calls run in a +thread via asyncio.to_thread + +Connects to: + config.py - settings.geoip_db_path + factory.py - initialized and closed in + lifespan + core/ingestion/ + pipeline - lookup called in feature_worker """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/__init__.py index e1add2a9..0def6a4e 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +Feature engineering package with per-request extraction, +windowed aggregation, encoding, patterns, and signatures """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/aggregator.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/aggregator.py index 9ddc540a..aea86550 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/aggregator.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/aggregator.py @@ -1,6 +1,27 @@ """ ©AngelaMos | 2026 aggregator.py + +Per-IP sliding window feature aggregator backed by Redis +sorted sets + +WindowAggregator.record_and_aggregate records each request +into 7 Redis sorted sets (requests, paths, statuses, UAs, +sizes, methods, depths) keyed by IP, trims entries older +than KEY_TTL (900s), and computes 12 windowed features in +a single pipelined round-trip: req_count at 1m/5m/10m +windows, error_rate_5m (4xx/5xx ratio), unique_paths_5m, +unique_uas_10m, method_entropy_5m (Shannon), avg_response +_size_5m, status_diversity_5m (distinct codes), path_depth +_variance_5m, and inter_request_time mean/std in ms. +Members are MD5-hashed for deduplication where needed + +Connects to: + core/ingestion/ + pipeline - called in feature_worker stage + core/features/ + mappings - WINDOWED_FEATURE_NAMES defines the + 12 windowed feature keys """ import hashlib diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/encoder.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/encoder.py index 0297db88..6ab2a534 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/encoder.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/encoder.py @@ -1,6 +1,23 @@ """ ©AngelaMos | 2026 encoder.py + +Feature vector encoder transforming a combined feature +dict into a 35-element float vector for ML inference + +encode_for_inference iterates FEATURE_ORDER, applying +boolean 0/1 encoding for 7 BOOLEAN_FEATURES, ordinal +lookup via CATEGORICAL_ENCODERS for http_method, status_ +class, and file_extension, deterministic country code +encoding via _encode_country (A-Z ordinal to 1-676), and +direct float cast for all numeric features + +Connects to: + core/features/ + mappings - FEATURE_ORDER, BOOLEAN_FEATURES, + CATEGORICAL_ENCODERS + core/ingestion/ + pipeline - called after feature merge """ from app.core.features.mappings import ( diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/extractor.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/extractor.py index 3b0f94e1..e006aad6 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/extractor.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/extractor.py @@ -1,6 +1,31 @@ """ ©AngelaMos | 2026 extractor.py + +Stateless per-request feature extraction producing 23 +features from a parsed log entry + +extract_request_features computes: http_method, path_depth, +path_entropy (Shannon), path_length, query_string_length, +query_param_count, has_encoded_chars, has_double_encoding, +status_code, status_class (Nxx), response_size, hour_of_ +day, day_of_week, is_weekend, ua_length, ua_entropy, +is_known_bot, is_known_scanner, has_attack_pattern, +special_char_ratio, file_extension, country_code, and +is_private_ip. Pattern detection uses compiled regexes +from patterns module, bot/scanner detection uses signature +sets + +Connects to: + core/features/ + patterns - ATTACK_COMBINED, DOUBLE_ENCODED, + ENCODED_CHARS + core/features/ + signatures - BOT_USER_AGENTS, SCANNER_USER_AGENTS + core/ingestion/ + parsers - ParsedLogEntry input + core/ingestion/ + pipeline - called in feature_worker stage """ import ipaddress diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/mappings.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/mappings.py index 38e57798..a45c3d15 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/mappings.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/mappings.py @@ -1,6 +1,25 @@ """ ©AngelaMos | 2026 mappings.py + +Feature encoding mappings, canonical feature order, and +classification constants for the 35-feature ML input spec + +Defines METHOD_MAP (7 HTTP verbs), STATUS_CLASS_MAP (5 +response classes), EXTENSION_MAP (25 file extensions), +FEATURE_ORDER (35-element canonical list: 23 per-request ++ 12 windowed), CATEGORICAL_ENCODERS routing http_method/ +status_class/file_extension to their ordinal maps, +WINDOWED_FEATURE_NAMES (last 12 features), and BOOLEAN_ +FEATURES (7 binary flags). These mappings ensure training +and inference use identical encoding + +Connects to: + core/features/ + encoder - FEATURE_ORDER, BOOLEAN_FEATURES, + CATEGORICAL_ENCODERS + ml/data_loader - FEATURE_ORDER for column alignment + ml/synthetic - FEATURE_ORDER for sample generation """ METHOD_MAP: dict[str, int] = { diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/patterns.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/patterns.py index cd375ca5..b499fcf8 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/patterns.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/patterns.py @@ -1,6 +1,32 @@ """ ©AngelaMos | 2026 patterns.py + +Compiled regex patterns for web attack detection covering +7 OWASP categories plus encoding anomalies + +Defines case-insensitive compiled patterns for: SQLI +(union select, sleep, benchmark, information_schema, hex +literals, comment injection), XSS (script tags, event +handlers, javascript/vbscript URIs, DOM sinks like +document.cookie/write, eval/alert/prompt), PATH_TRAVERSAL +(../ sequences, %2e encoding, sensitive file paths like +etc/passwd, .git/config, .env), COMMAND_INJECTION +(semicolon/pipe chaining to shell commands, $() and +backtick substitution, ${} expansion), FILE_INCLUSION +(php://, file://, data://, phar:// wrapper schemes), SSRF +(cloud metadata IPs 169.254.169.254, localhost with paths, +dict:// and gopher://), LOG4SHELL (${jndi, ${lower, ${:- +patterns). Also provides ENCODED_CHARS, DOUBLE_ENCODED for +evasion detection, and ATTACK_COMBINED unioning all 7 +patterns + +Connects to: + core/features/ + extractor - ATTACK_COMBINED, DOUBLE_ENCODED, + ENCODED_CHARS + core/detection/ + rules - individual patterns for scored rules """ import re diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/signatures.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/signatures.py index 2c6a5bb1..ef3f9c84 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/signatures.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/features/signatures.py @@ -1,6 +1,23 @@ """ ©AngelaMos | 2026 signatures.py + +User-agent signature sets for bot and security scanner +detection + +BOT_USER_AGENTS contains 34 lowercase search engine and +crawler identifiers (googlebot, bingbot, gptbot, claudebot, +etc.) for benign bot classification. SCANNER_USER_AGENTS +contains 41 lowercase security tool signatures (nikto, +sqlmap, nmap, burp, nuclei, metasploit, hydra, etc.) for +hostile scanner detection. Both are frozensets matched via +substring search against lowercased user-agent strings + +Connects to: + core/features/ + extractor - is_known_bot, is_known_scanner features + core/detection/ + rules - SCANNER_USER_AGENTS for UA rule scoring """ BOT_USER_AGENTS: frozenset[str] = frozenset({ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/__init__.py index e1add2a9..3e6fbb10 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +Ingestion package with log parsing, file tailing, and the +four-stage async processing pipeline """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/parsers.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/parsers.py index 0685d922..0bc30273 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/parsers.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/parsers.py @@ -1,6 +1,28 @@ """ ©AngelaMos | 2026 parsers.py + +Nginx combined-format log line parser with fast string- +split primary and compiled regex fallback + +ParsedLogEntry is a frozen slotted dataclass holding ip, +timestamp, method, path, query_string, status_code, +response_size, referer, user_agent, and raw_line. +parse_combined tries _parse_split first (splitting on +quote boundaries for speed), falling back to _parse_regex +with a compiled _COMBINED_RE pattern. Both extract the +request line, split URI into path and query_string, parse +timestamp via strptime with timezone, and handle dash +placeholders for size and referer + +Connects to: + core/ingestion/ + pipeline - parse_combined in parse_worker stage + core/detection/ + rules - ParsedLogEntry consumed by RuleEngine + core/features/ + extractor - ParsedLogEntry consumed by feature + extraction """ import re diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/pipeline.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/pipeline.py index ddf1cc48..56862e47 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/pipeline.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/pipeline.py @@ -1,6 +1,34 @@ """ ©AngelaMos | 2026 pipeline.py + +Four-stage async pipeline transforming raw nginx log lines +into scored threat candidates + +Stage 1 (_parse_worker): parses raw lines via parse_ +combined into ParsedLogEntry. Stage 2 (_feature_worker): +enriches with GeoIP lookup, extracts 23 per-request +features, aggregates 12 windowed features via Redis-backed +WindowAggregator, and encodes the merged 35-dim float +vector. Stage 3 (_detection_worker): scores via RuleEngine, +optionally runs ML ensemble inference (normalize AE/IF +scores, fuse with configurable weights, blend with rule +score at 0.7 ML weight). Stage 4 (_dispatch_worker): +forwards ScoredRequests via the on_result callback. Stages +are connected by sized asyncio.Queues with poison-pill +shutdown propagation. EnrichedRequest and ScoredRequest +dataclasses carry data between stages + +Connects to: + core/ingestion/parsers - parse_combined + core/enrichment/geoip - GeoIPService.lookup + core/features/extractor - extract_request_features + core/features/aggregator - WindowAggregator + core/features/encoder - encode_for_inference + core/detection/rules - RuleEngine.score_request + core/detection/inference - InferenceEngine.predict + core/detection/ensemble - normalize/fuse/blend scores + core/alerts/dispatcher - on_result callback """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/tailer.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/tailer.py index ebc0004b..38e56b25 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/tailer.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/ingestion/tailer.py @@ -1,6 +1,26 @@ """ ©AngelaMos | 2026 tailer.py + +Watchdog-based nginx log file tailer with rotation +detection pushing raw lines into an asyncio queue + +_LogHandler extends FileSystemEventHandler to tail a single +target file: _open_target seeks to EOF, on_modified reads +new lines via _read_new_lines and checks inode changes for +rotation, on_moved handles rename-based rotation (access +.log -> access.log.1), on_created handles new-file +rotation. Lines are pushed via call_soon_threadsafe into +the asyncio queue, with QueueFull drops logged. LogTailer +wraps _LogHandler with a PollingObserver (2s interval) +watching the target's parent directory, providing start/ +stop lifecycle and is_active property + +Connects to: + factory.py - started/stopped in lifespan + core/ingestion/ + pipeline - feeds pipeline.raw_queue + config.py - settings.nginx_log_path """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/core/redis_manager.py b/PROJECTS/advanced/ai-threat-detection/backend/app/core/redis_manager.py index 163c1a10..6095fb2e 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/core/redis_manager.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/core/redis_manager.py @@ -1,6 +1,21 @@ """ ©AngelaMos | 2026 redis_manager.py + +Async Redis connection lifecycle manager with module-level +singleton + +RedisManager wraps redis.asyncio connection creation +(from_url with decode_responses), graceful close, client +property access, and PING health check. The module +exports redis_manager as a singleton used by factory +lifespan, alert dispatcher, and websocket endpoint + +Connects to: + config.py - settings.redis_url + factory.py - connect/disconnect in lifespan + api/websocket - client for pub/sub + api/health - ping() for readiness probe """ import redis.asyncio as aioredis diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/factory.py b/PROJECTS/advanced/ai-threat-detection/backend/app/factory.py index d689c65e..5b904601 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/factory.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/factory.py @@ -1,6 +1,36 @@ """ ©AngelaMos | 2026 factory.py + +FastAPI application factory with async lifespan managing +database, Redis, pipeline, and ML model initialization + +lifespan creates the async SQLAlchemy engine and session +factory, runs SQLModel.metadata.create_all, connects +Redis, initializes GeoIPService, constructs the Alert +Dispatcher, attempts to load the ONNX InferenceEngine +(falling back to rules-only mode), builds the Pipeline +with configured queue sizes and ensemble weights, starts +the LogTailer if the nginx log directory exists, and +stores all components on app.state. On shutdown it stops +the tailer, pipeline, GeoIP, Redis, and disposes the DB +engine. _load_inference_engine lazily imports onnxruntime +-backed InferenceEngine, returning None if the dependency +is missing or no models exist. create_app assembles the +FastAPI instance and mounts all six API routers (health, +ingest, threats, stats, models, websocket) + +Connects to: + config.py - settings for all config values + core/ingestion/pipeline - Pipeline + core/ingestion/tailer - LogTailer + core/detection/rules - RuleEngine + core/detection/inference- InferenceEngine (optional) + core/alerts/dispatcher - AlertDispatcher + core/enrichment/geoip - GeoIPService + core/redis_manager - redis_manager + api/ - all route modules + models/ - SQLModel registration """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/main.py b/PROJECTS/advanced/ai-threat-detection/backend/app/main.py index 2ee26550..5b0b7a58 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/main.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/main.py @@ -1,6 +1,11 @@ """ ©AngelaMos | 2026 main.py + +ASGI application instance created by the factory + +Connects to: + factory.py - create_app builds the FastAPI instance """ from app.factory import create_app diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/models/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/models/__init__.py index 0b879feb..8ad98183 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/models/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/models/__init__.py @@ -1,6 +1,9 @@ """ ©AngelaMos | 2026 __init__.py + +Models package exporting SQLModel table classes for +ThreatEvent and ModelMetadata """ from app.models.model_metadata import ModelMetadata diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/models/base.py b/PROJECTS/advanced/ai-threat-detection/backend/app/models/base.py index 764cdc49..6d26d70d 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/models/base.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/models/base.py @@ -1,6 +1,18 @@ """ ©AngelaMos | 2026 base.py + +Abstract SQLModel base class providing UUID primary key +and timezone-aware created_at timestamp + +TimestampedModel defines id as a uuid4 primary key and +created_at as a DateTime(timezone=True) column with +CURRENT_TIMESTAMP server default. All domain models +inherit from this base + +Connects to: + models/threat_event - ThreatEvent inherits + models/model_metadata - ModelMetadata inherits """ import uuid diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/models/model_metadata.py b/PROJECTS/advanced/ai-threat-detection/backend/app/models/model_metadata.py index 6311239a..a5de5d90 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/models/model_metadata.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/models/model_metadata.py @@ -1,6 +1,21 @@ """ ©AngelaMos | 2026 model_metadata.py + +SQLModel table tracking ML model versions, training +metrics, and deployment status + +ModelMetadata stores model_type, version, training_samples, +metrics (JSON), artifact_path, is_active flag, optional +mlflow_run_id, threshold, and notes. A partial index on +model_type filtered by is_active=TRUE enables fast lookup +of the currently deployed model per type + +Connects to: + models/base - inherits TimestampedModel + api/models_api - queried for /models/status, + written after retrain + cli/main - _write_metadata inserts records """ from sqlalchemy import Column, Index, JSON, text diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/models/threat_event.py b/PROJECTS/advanced/ai-threat-detection/backend/app/models/threat_event.py index f273ec28..28ca3b37 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/models/threat_event.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/models/threat_event.py @@ -1,6 +1,25 @@ """ ©AngelaMos | 2026 threat_event.py + +SQLModel table for detected threat events with full +request context and ML metadata + +ThreatEvent stores source_ip, request_method, request_path, +status_code, response_size, user_agent, threat_score, +severity, component_scores (JSON), geo fields (country, +city, lat, lon), feature_vector (JSON float array), +matched_rules (JSON string array), model_version, +reviewed flag, and review_label for analyst feedback. +Indexed on created_at, source_ip, severity, threat_score, +and a partial index on reviewed=FALSE for triage queries + +Connects to: + models/base - inherits TimestampedModel + services/threat_service - CRUD operations + api/models_api - training data source for + retrain + core/alerts/dispatcher - persisted on MEDIUM+ severity """ from sqlalchemy import ( diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/__init__.py index e1add2a9..b37e7cb2 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +Pydantic schemas package for API request/response +validation across stats, threats, and websocket endpoints """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/stats.py b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/stats.py index 5c74c46d..acb2e018 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/stats.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/stats.py @@ -1,6 +1,18 @@ """ ©AngelaMos | 2026 stats.py + +Pydantic response models for the /stats endpoint + +SeverityBreakdown holds high/medium/low threat counts. +IPStatEntry and PathStatEntry pair a source_ip or path +with a count. StatsResponse aggregates time_range, +threats_stored, threats_detected, severity_breakdown, +top_source_ips (top 10), and top_attacked_paths (top 10) + +Connects to: + api/stats - StatsResponse as response_model + services/stats_service - constructs StatsResponse """ from pydantic import BaseModel diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/threats.py b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/threats.py index b2d42fe9..dc9d76c5 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/threats.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/threats.py @@ -1,6 +1,21 @@ """ ©AngelaMos | 2026 threats.py + +Pydantic response models for the /threats endpoints + +GeoInfo holds optional country, city, lat, lon from GeoIP +lookups. ThreatEventResponse is the full event schema with +UUID id, timestamps, request details, threat_score, +severity (Literal HIGH/MEDIUM/LOW), component_scores, +geo info, matched_rules, model_version, and review status +(from_attributes enabled for ORM conversion). Threat +ListResponse wraps paginated items with total/limit/offset + +Connects to: + api/threats - response_model for list and + detail endpoints + services/threat_service - _to_response builds these """ import uuid diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/websocket.py b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/websocket.py index 3d83cbee..400dd80d 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/websocket.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/schemas/websocket.py @@ -1,6 +1,18 @@ """ ©AngelaMos | 2026 websocket.py + +Pydantic model for real-time WebSocket threat alert +payloads + +WebSocketAlert carries event type (Literal "threat"), +timestamp, source_ip, request_method, request_path, +threat_score, severity, and component_scores. Serialized +via model_dump_json for Redis pub/sub broadcast + +Connects to: + core/alerts/dispatcher - constructs and publishes alerts + api/websocket - relayed to connected clients """ from datetime import datetime diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/services/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/app/services/__init__.py index e1add2a9..553c2042 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/services/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/services/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +Service layer package with threat event CRUD and +statistics aggregation business logic """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/services/stats_service.py b/PROJECTS/advanced/ai-threat-detection/backend/app/services/stats_service.py index 40255ae4..8f0ceb7e 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/services/stats_service.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/services/stats_service.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 stats_service.py + +Threat statistics aggregation service computing time- +windowed metrics from stored events + +get_stats accepts a time_range string (1h, 6h, 24h, 7d, +30d) mapped to timedeltas via _RANGE_MAP, queries threat +events since the cutoff, and returns a StatsResponse with +total count, severity breakdown (HIGH/MEDIUM/LOW counts +via GROUP BY), top 10 source IPs, and top 10 attacked +paths ordered by frequency + +Connects to: + models/threat_event - ThreatEvent queries + schemas/stats - StatsResponse, SeverityBreakdown, + IPStatEntry, PathStatEntry + api/stats - called from GET /stats endpoint """ from datetime import datetime, timedelta, UTC diff --git a/PROJECTS/advanced/ai-threat-detection/backend/app/services/threat_service.py b/PROJECTS/advanced/ai-threat-detection/backend/app/services/threat_service.py index d7971dd1..30396781 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/app/services/threat_service.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/app/services/threat_service.py @@ -1,6 +1,29 @@ """ ©AngelaMos | 2026 threat_service.py + +Threat event CRUD service for database persistence and +retrieval + +get_threats builds a filtered, paginated query with +optional severity, source_ip, since/until datetime +filters, ordered by created_at DESC. get_threat_by_id +fetches a single event by UUID. create_threat_event +persists a ScoredRequest as a ThreatEvent with full +request context, GeoIP data, feature vector, matched +rules, and severity classification. _to_response converts +ThreatEvent ORM models to ThreatEventResponse schemas +with nested GeoInfo + +Connects to: + models/threat_event - ThreatEvent table operations + schemas/threats - ThreatEventResponse, GeoInfo, + ThreatListResponse + core/detection/ensemble - classify_severity for create + core/ingestion/pipeline - ScoredRequest input type + api/threats - called from list/detail + endpoints + core/alerts/dispatcher - called on MEDIUM+ dispatch """ import uuid diff --git a/PROJECTS/advanced/ai-threat-detection/backend/cli/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/cli/__init__.py index e1add2a9..328a1b39 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/cli/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/cli/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +CLI package providing the Typer-based vigil command-line +interface for server, training, replay, and diagnostics """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/cli/main.py b/PROJECTS/advanced/ai-threat-detection/backend/cli/main.py index 050d8db6..3657ee4c 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/cli/main.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/cli/main.py @@ -1,6 +1,29 @@ """ ©AngelaMos | 2026 main.py + +Typer CLI application with serve, train, replay, config, +and health commands + +serve launches uvicorn with configurable host/port/reload. +train loads CSIC 2010 dataset and/or synthetic data, runs +TrainingOrchestrator, exports ONNX models, and writes +metadata to the database via _write_metadata (creates an +async engine, calls save_model_metadata). replay sends +historical log lines in batches to a running server's +/ingest/batch endpoint via httpx. config prints all +settings with secrets redacted (_redact_url masks +credentials in database URLs). health pings /health and +displays status, uptime, and pipeline state + +Connects to: + app/config - settings for serve defaults + app/main - uvicorn target "app.main:app" + ml/orchestrator - TrainingOrchestrator for train + ml/data_loader - load_csic_dataset for CSIC data + ml/synthetic - generate_mixed_dataset + ml/metadata - save_model_metadata + api/ingest - /ingest/batch for replay """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/__init__.py index e1add2a9..de7dc0db 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/__init__.py @@ -1,4 +1,7 @@ """ ©AngelaMos | 2026 __init__.py + +ML package with autoencoder, classifier training, ONNX +export, data loading, experiment tracking, and validation """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/autoencoder.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/autoencoder.py index 586468d3..cd33f3cc 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/autoencoder.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/autoencoder.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 autoencoder.py + +PyTorch symmetric autoencoder for HTTP request anomaly +detection + +ThreatAutoencoder has a 35->24->12->6 encoder and 6->12 +->24->35 decoder with BatchNorm1d, LeakyReLU(0.2), and +Dropout(0.2) between each linear layer. Trained on normal +traffic only so that high reconstruction error (compute_ +reconstruction_error via per-sample MSE) indicates +anomalous requests. encode/decode expose bottleneck access +for analysis + +Connects to: + ml/export_onnx - exported to ae.onnx + ml/orchestrator - trained in _train_autoencoder + ml/scaler - input normalized before training """ import torch diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/data_loader.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/data_loader.py index 8feec3f5..e8e0891d 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/data_loader.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/data_loader.py @@ -1,6 +1,28 @@ """ ©AngelaMos | 2026 data_loader.py + +CSIC 2010 HTTP dataset loader with feature extraction for +ML training + +parse_csic_file reads a CSIC dataset file, splits on HTTP +request line boundaries, and produces CSICRequest objects +(method, path, query_string, headers, body, label). +csic_to_parsed_entry converts CSICRequests to +ParsedLogEntrys with synthetic defaults (private IP, +random timestamp over 90 days, 200 status). load_csic_ +dataset loads normal (label=0) and attack (label=1) +files, extracts 23 per-request features, zeros 12 +windowed features, encodes to 35-dim vectors, and returns +(X, y) numpy arrays. load_csic_normal loads a single +normal-only file + +Connects to: + core/features/extractor - extract_request_features + core/features/encoder - encode_for_inference + core/features/mappings - WINDOWED_FEATURE_NAMES + core/ingestion/parsers - ParsedLogEntry + cli/main - loaded in train command """ import logging diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/download_csic.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/download_csic.py index 75b1cb31..ad547c91 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/download_csic.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/download_csic.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 download_csic.py + +CSIC 2010 dataset downloader with progress display and +integrity checking + +download_csic fetches normalTrafficTraining.txt, normal +TrafficTest.txt, and anomalousTrafficTest.txt from the +Universidad de la Republica GitLab mirror via httpx +streaming, writing to data/datasets/csic2010/. Skips +files that already exist above MIN_FILE_BYTES (1MB). +Shows download progress (percentage or MB), computes +SHA-256 via _compute_sha256, and warns on suspiciously +small downloads + +Connects to: + ml/data_loader - downloaded files consumed by + parse_csic_file """ import hashlib diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/experiment.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/experiment.py index c1758ad2..ce5e7ccb 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/experiment.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/experiment.py @@ -1,6 +1,19 @@ """ ©AngelaMos | 2026 experiment.py + +MLflow experiment context manager with automatic system +metadata logging + +VigilExperiment wraps mlflow.start_run/end_run as a context +manager, recording Python version, platform, and git commit +hash on entry, and setting status/error tags on exit. +Provides log_params, log_metrics (with optional step), and +log_artifact convenience methods. _get_git_hash shells out +to git rev-parse --short HEAD + +Connects to: + ml/orchestrator - used to wrap the full training run """ import platform diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/export_onnx.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/export_onnx.py index 593dba8f..4d13dc6c 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/export_onnx.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/export_onnx.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 export_onnx.py + +ONNX model export functions for the 3-model ML ensemble + +export_autoencoder converts a PyTorch ThreatAutoencoder to +ONNX with dynamic batch dimension, opset 17, constant +folding, and named I/O (features/reconstructed). export_ +random_forest and export_isolation_forest convert sklearn +estimators to ONNX via skl2onnx with FloatTensorType input +and target opset {"": 17, "ai.onnx.ml": 3}. All functions +create parent directories and return the output Path + +Connects to: + ml/autoencoder - ThreatAutoencoder model class + ml/orchestrator - called after training completes + core/detection/ + inference - loads the exported ONNX files """ from pathlib import Path diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/metadata.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/metadata.py index 9d3c4e22..f9291d38 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/metadata.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/metadata.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 metadata.py + +Model metadata persistence for tracking trained model +versions and deployment status + +compute_model_version produces a 12-char hex version from +the SHA-256 of an ONNX artifact file. save_model_metadata +iterates MODEL_TYPES (ae.onnx -> autoencoder, rf.onnx -> +random_forest, if.onnx -> isolation_forest), deactivates +any previously active version of each type, and inserts +new ModelMetadata rows with version, training_samples, +metrics, artifact_path, mlflow_run_id, and threshold + +Connects to: + models/model_metadata - ModelMetadata ORM model + cli/main - called from _write_metadata + api/models_api - called after retrain """ import hashlib diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/orchestrator.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/orchestrator.py index 4396b46b..6a562d6a 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/orchestrator.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/orchestrator.py @@ -1,6 +1,32 @@ """ ©AngelaMos | 2026 orchestrator.py + +End-to-end training pipeline orchestrator for the 3-model +ML ensemble + +TrainingOrchestrator.run accepts (X, y) arrays, calls +prepare_training_data for stratified splitting with SMOTE, +trains the autoencoder on normal-only data, random forest +on labeled data, and isolation forest on normal-only data, +exports all three to ONNX (ae.onnx, rf.onnx, if.onnx) +plus scaler.json and threshold.json, runs validate_ensemble +against the held-out test set with PR-AUC and F1 quality +gates, and logs all parameters, metrics, and artifacts to +MLflow via VigilExperiment. Returns a TrainingResult +dataclass aggregating per-model metrics, gate status, +output directory, and MLflow run ID + +Connects to: + ml/experiment - VigilExperiment context manager + ml/export_onnx - ONNX export functions + ml/splitting - prepare_training_data + ml/train_autoencoder - train_autoencoder + ml/train_classifiers - train_random_forest, + train_isolation_forest + ml/validation - validate_ensemble + cli/main - called from train command + api/models_api - called from retrain endpoint """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/scaler.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/scaler.py index 3619161c..695d2812 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/scaler.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/scaler.py @@ -1,6 +1,27 @@ """ ©AngelaMos | 2026 scaler.py + +IQR-based feature scaler with JSON persistence for the +autoencoder preprocessing stage + +FeatureScaler wraps sklearn RobustScaler (median/IQR +normalization) to handle outlier-heavy HTTP traffic data. +Provides fit, transform, fit_transform, and +inverse_transform mirroring the sklearn API. save_json +serializes center and scale arrays to a human-readable +JSON file (avoiding pickle for security and portability), +and load_json reconstructs a fitted scaler from that file. +Only the autoencoder uses this scaler since tree-based +models (random forest, isolation forest) are +scale-invariant + +Connects to: + ml/train_autoencoder - fitted during AE training + ml/orchestrator - scaler.json saved alongside models + core/detection/ + inference - loaded at inference time for AE + input normalization """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/splitting.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/splitting.py index 51470681..c23b707d 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/splitting.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/splitting.py @@ -1,6 +1,23 @@ """ ©AngelaMos | 2026 splitting.py + +Stratified train/val/test splitting with SMOTE +oversampling for imbalanced attack data + +prepare_training_data performs a 70/15/15 stratified split +preserving class ratios, extracts the normal-only subset +from training data for the autoencoder and isolation +forest, and conditionally applies SMOTE oversampling to +the training set when the minority class ratio falls below +the target strategy (default 0.3). SMOTE is skipped if the +minority class has fewer than k_neighbors+1 samples. +Returns a TrainingSplit dataclass with X_train, y_train, +X_val, y_val, X_test, y_test, and X_normal_train arrays + +Connects to: + ml/orchestrator - called at the start of the training + pipeline """ from dataclasses import dataclass diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/synthetic.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/synthetic.py index c916c3b7..556d7fdf 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/synthetic.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/synthetic.py @@ -1,6 +1,31 @@ """ ©AngelaMos | 2026 synthetic.py + +Synthetic HTTP traffic generator for ML training and +testing with realistic attack payloads + +Provides per-category generators for 6 attack types: +generate_sqli_requests (22 SQL injection payloads), +generate_xss_requests (21 XSS vectors), generate_ +traversal_requests (15 path traversal payloads), +generate_log4shell_requests (10 JNDI lookup variants), +generate_ssrf_requests (11 cloud metadata and internal +service targets), and generate_scanner_requests (11 +vulnerability scanner user-agents). generate_normal_ +requests produces benign traffic across 31 realistic +paths. generate_mixed_dataset orchestrates all generators, +converts ParsedLogEntry objects to 35-dim feature vectors +via extract_request_features and encode_for_inference with +zeroed windowed features, and returns (X, y) numpy arrays + +Connects to: + core/features/extractor - extract_request_features + core/features/encoder - encode_for_inference + core/features/mappings - WINDOWED_FEATURE_NAMES + core/ingestion/parsers - ParsedLogEntry + cli/main - used when no CSIC dataset is + available """ import logging diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/train_autoencoder.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/train_autoencoder.py index 5d90338d..26f2ebb7 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/train_autoencoder.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/train_autoencoder.py @@ -1,6 +1,27 @@ """ ©AngelaMos | 2026 train_autoencoder.py + +PyTorch autoencoder training loop with early stopping and +anomaly threshold calibration + +train_autoencoder takes normal-only traffic vectors, splits +off a 15% validation set, fits a FeatureScaler (IQR-based) +on training data, builds DataLoaders, and trains a +ThreatAutoencoder (35->24->12->6->12->24->35) using MSE +loss with AdamW optimizer (weight decay 1e-5), +ReduceLROnPlateau scheduler (factor 0.5, patience 5), +gradient clipping at max_norm 1.0, and early stopping +(default patience 10). After training, computes per-sample +reconstruction error on the validation set and sets the +anomaly threshold at the 99.5th percentile. Returns the +trained model, fitted scaler, calibrated threshold, and +train/val loss history + +Connects to: + ml/autoencoder - ThreatAutoencoder model class + ml/scaler - FeatureScaler for input normalization + ml/orchestrator - called during pipeline execution """ from typing import Any diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/train_classifiers.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/train_classifiers.py index 6d2b3fed..7aa7a182 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/train_classifiers.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/train_classifiers.py @@ -1,6 +1,24 @@ """ ©AngelaMos | 2026 train_classifiers.py + +Sklearn classifier training for the random forest and +isolation forest ensemble members + +train_random_forest builds a 200-tree balanced-weight +RandomForestClassifier with max_depth 20, wraps it in +CalibratedClassifierCV with isotonic calibration (3-fold +CV) for well-calibrated probability outputs, evaluates on +a held-out 20% calibration split, and returns the +calibrated model with accuracy, precision, recall, F1, and +PR-AUC metrics. train_isolation_forest fits a 200-tree +IsolationForest on normal-only traffic with automatic +contamination estimation, returning the model and sample +count + +Connects to: + ml/orchestrator - called during pipeline execution + ml/export_onnx - models exported to ONNX after training """ from typing import Any diff --git a/PROJECTS/advanced/ai-threat-detection/backend/ml/validation.py b/PROJECTS/advanced/ai-threat-detection/backend/ml/validation.py index 52906908..35bb903e 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/ml/validation.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/ml/validation.py @@ -1,6 +1,28 @@ """ ©AngelaMos | 2026 validation.py + +Post-training ensemble validation with quality gates for +deployment readiness + +validate_ensemble loads all 3 ONNX models via +InferenceEngine, runs batch prediction on held-out test +data, normalizes per-model raw scores (AE reconstruction +error against threshold, IF anomaly scores), fuses them +via weighted average (default weights: AE 0.4, RF 0.4, +IF 0.2), applies a 0.5 binary threshold, and computes +precision, recall, F1, PR-AUC, and ROC-AUC. Quality +gates require PR-AUC >= 0.85 and F1 >= 0.80 for +passed_gates to be True. Returns a ValidationResult +dataclass with all metrics, confusion matrix, and +per-gate pass/fail details + +Connects to: + core/detection/ensemble - normalize_ae_score, + normalize_if_score, fuse_scores + core/detection/inference - InferenceEngine ONNX runtime + ml/orchestrator - called after training to gate + deployment """ import logging diff --git a/PROJECTS/advanced/ai-threat-detection/backend/pyproject.toml b/PROJECTS/advanced/ai-threat-detection/backend/pyproject.toml index b0ab9158..8834f5f3 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/pyproject.toml +++ b/PROJECTS/advanced/ai-threat-detection/backend/pyproject.toml @@ -1,5 +1,19 @@ # ©AngelaMos | 2026 # pyproject.toml +# +# Python project metadata, dependencies, and tool +# configuration for AngelusVigil +# +# Declares the angelusvigil package (Python 3.14+) with +# core dependencies (FastAPI, uvicorn, SQLAlchemy, asyncpg, +# Redis, Pydantic, watchdog, geoip2, typer), dev extras +# (pytest, ruff, mypy, pylint, coverage, fakeredis), and ml +# extras (torch, scikit-learn, onnxruntime, mlflow, pandas, +# imbalanced-learn). Uses hatchling as the build backend +# with app, cli, and ml packages. Configures ruff (line 95, +# Python 3.14 target), mypy (strict mode), pylint (4 jobs +# with pydantic plugin), and pytest (asyncio auto mode). +# Connects to all backend source modules [project] name = "angelusvigil" diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/__init__.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/__init__.py index e1add2a9..034d2c77 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/__init__.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/__init__.py @@ -1,4 +1,17 @@ """ ©AngelaMos | 2026 __init__.py + +Test suite package for the ai-threat-detection backend + +Contains unit, integration, and end-to-end tests covering +the full stack: API endpoints, ingestion pipeline, feature +extraction, rule engine, ML training and inference, +ensemble scoring, ONNX export, model metadata persistence, +CLI commands, and GeoIP enrichment. Uses pytest-asyncio for +async tests, fakeredis for Redis isolation, and in-memory +SQLite via aiosqlite for database tests + +Connects to: + tests/conftest - shared fixtures for DB and HTTP client """ diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/conftest.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/conftest.py index 399062b9..9a306b2b 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/conftest.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/conftest.py @@ -2,7 +2,22 @@ ©AngelaMos | 2026 conftest.py -Shared pytest fixtures for in-memory SQLite database and HTTPX test client setup. +Shared pytest fixtures for in-memory SQLite database and +HTTPX async test client setup + +test_settings overrides Settings for the test environment +with an in-memory SQLite URL and dummy paths. db_engine +creates a StaticPool aiosqlite engine with all tables via +SQLModel.metadata.create_all. db_session yields an +AsyncSession bound to the shared engine. db_client builds +a full HTTPX AsyncClient with ASGITransport wrapping the +FastAPI app, overriding get_session to use the in-memory +database with auto-commit + +Connects to: + app/config - Settings override + app/factory - create_app for ASGI transport + app/api/deps - get_session dependency override """ from collections.abc import AsyncIterator diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_api.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_api.py index eb0e1f94..c5074157 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_api.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_api.py @@ -2,7 +2,23 @@ ©AngelaMos | 2026 test_api.py -Tests the FastAPI REST endpoints for threats, stats, health, readiness, and model management. +Tests the FastAPI REST endpoints for health, threats, stats, +and model management using an in-memory database + +Validates /health returns status, uptime, and pipeline flag. +Tests /threats CRUD: empty list returns zero total, random +UUID returns 404, seeded event is fetchable by ID with all +fields, and severity filter returns only matching items. +Tests /stats returns zeroed counts on empty window. +Tests /models/status returns detection_mode and +active_models list, and POST /models/retrain returns 202 +with a 32-char job ID + +Connects to: + api/health - liveness endpoint + api/threats - threat CRUD + api/stats - statistics endpoint + api/models_api - model status and retrain """ import uuid diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_autoencoder.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_autoencoder.py index f8d96a01..df5b950b 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_autoencoder.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_autoencoder.py @@ -2,7 +2,21 @@ ©AngelaMos | 2026 test_autoencoder.py -Tests the ThreatAutoencoder architecture: shapes, output range, reconstruction error, and training behavior. +Tests the ThreatAutoencoder PyTorch architecture for shape +correctness, output range, reconstruction error, and +training behavior + +Validates output shape matches input (batch, 35), encoder +bottleneck compresses to 6 dimensions, single-sample +forward pass succeeds in eval mode, decoder output is +unbounded (matching RobustScaler range), reconstruction +error returns one positive scalar per sample, trained model +reconstructs normal data better than anomalies after 50 +epochs, eval mode produces deterministic output (dropout +off), and variable batch sizes (1, 8, 32, 128) are handled + +Connects to: + ml/autoencoder - ThreatAutoencoder """ import pytest diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_cli.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_cli.py index 1d2da7f6..fe92fe1f 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_cli.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_cli.py @@ -1,6 +1,20 @@ """ ©AngelaMos | 2026 test_cli.py + +Tests the Typer CLI command help output, argument +validation, and metadata persistence wiring + +TestCLICommands validates train --help shows csic-dir and +synthetic options, nonexistent csic-dir exits with error, +replay/serve/config/health --help exit cleanly with +expected content, and missing replay log file fails. +TestCLITrainMetadata mocks the orchestrator to verify that +train emits a warning when DB metadata write is unavailable + +Connects to: + cli/main - Typer app with serve, train, replay, config, + health commands """ import re diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_config_ml.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_config_ml.py index 17a2203e..e4ed01e2 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_config_ml.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_config_ml.py @@ -2,7 +2,17 @@ ©AngelaMos | 2026 test_config_ml.py -Tests ML-related settings defaults: detection mode, ensemble weights, model paths, and MLflow URI. +Tests ML-related settings defaults for detection mode, +ensemble weights, model paths, and MLflow tracking URI + +Validates that the default detection_mode is 'rules', +ensemble weights (AE + RF + IF) sum to exactly 1.0, +model_dir defaults to 'data/models', ae_threshold_ +percentile defaults to 99.5, and mlflow_tracking_uri +defaults to 'file:./mlruns' + +Connects to: + app/config - Settings pydantic-settings model """ from app.config import settings diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_data_loader.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_data_loader.py index 8b01ad6c..f35745bc 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_data_loader.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_data_loader.py @@ -1,6 +1,23 @@ """ ©AngelaMos | 2026 test_data_loader.py + +Tests CSIC 2010 dataset parsing, CSICRequest-to- +ParsedLogEntry conversion, and end-to-end dataset loading + +TestParseCSICFile validates HTTP request block splitting, +method/path/query/header extraction, POST body capture, +attack label assignment, malformed block skipping, and +empty file handling using inline CSIC-format fixtures. +TestCSICToParsedEntry verifies synthesized defaults (IP, +timestamp, status) and POST body query string merging. +TestLoadCSICDataset confirms 35-column X shape, dual-label +y arrays, correct per-file label counts, and finite feature +values + +Connects to: + ml/data_loader - parse_csic_file, csic_to_parsed_entry, + load_csic_dataset """ from pathlib import Path diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_detection.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_detection.py index 6666d03c..22b0bebc 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_detection.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_detection.py @@ -2,7 +2,25 @@ ©AngelaMos | 2026 test_detection.py -Tests the rule engine's threat scoring, severity classification, and attack pattern matching. +Tests the RuleEngine threat scoring, severity +classification, and OWASP attack pattern matching + +Validates normal requests score LOW below 0.5, SQL +injection in query strings scores HIGH with SQL_INJECTION +rule, XSS payloads trigger XSS rule, path traversal +triggers PATH_TRAVERSAL, command injection triggers +COMMAND_INJECTION at HIGH severity, scanner UAs fire +SCANNER_UA, high request rates fire RATE_ANOMALY, multiple +rules aggregate to higher scores, scores are clamped to +[0, 1], severity thresholds align with architecture +(LOW < 0.5, MEDIUM >= 0.5, HIGH >= 0.7), component_scores +match matched_rules, FILE_INCLUSION detects PHP stream +wrappers, and DOUBLE_ENCODING detects %25-prefixed +sequences + +Connects to: + core/detection/rules - RuleEngine + core/ingestion/parsers - ParsedLogEntry """ from datetime import datetime, UTC diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ensemble.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ensemble.py index bce084ec..999f1147 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ensemble.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ensemble.py @@ -2,7 +2,23 @@ ©AngelaMos | 2026 test_ensemble.py -Tests ensemble score normalization, weighted fusion, ML/rule blending, and severity classification. +Tests ensemble score normalization, weighted fusion, ML/rule +blending, and severity classification functions + +TestScoreNormalization validates AE error below threshold +maps below 0.5, 3x threshold caps at 1.0, zero error maps +to 0.0, negative IF score maps above 0.5, positive below +0.5, and zero maps to 0.5. TestEnsembleFusion validates +weighted average computation, all-zero scores fuse to 0.0, +all-one scores fuse to 1.0, and partial model support. +TestBlendScores validates ML/rule blending at various +weights and clamping. TestClassifySeverity validates HIGH +at >= 0.7, MEDIUM at [0.5, 0.7), LOW below 0.5 + +Connects to: + core/detection/ensemble - normalize_ae_score, + normalize_if_score, fuse_scores, + blend_scores, classify_severity """ from app.core.detection.ensemble import ( diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_experiment.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_experiment.py index 0bffa51d..8cd7cca5 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_experiment.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_experiment.py @@ -2,7 +2,19 @@ ©AngelaMos | 2026 test_experiment.py -Tests the VigilExperiment MLflow wrapper: run lifecycle, param/metric logging, and status tagging. +Tests the VigilExperiment MLflow context manager for run +lifecycle, parameter/metric logging, and status tagging + +Uses a tmp_path MLflow tracking URI for isolation. +Validates run ID is set on context entry and None before, +log_params writes string values, log_metrics stores floats, +log_artifact uploads files to the artifact list, +python_version and platform system metadata tags are auto- +logged, successful exit tags status='completed', and +exception exit tags status='failed' with the error message + +Connects to: + ml/experiment - VigilExperiment """ from pathlib import Path diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_export_onnx.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_export_onnx.py index 09c50569..9e9dce17 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_export_onnx.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_export_onnx.py @@ -2,7 +2,22 @@ ©AngelaMos | 2026 test_export_onnx.py -Tests ONNX export for the autoencoder, random forest, and isolation forest models. +Tests ONNX export and inference parity for the autoencoder, +random forest, and isolation forest models + +TestAutoencoderExport validates file creation, PyTorch-to- +ONNX output match within 1e-5 tolerance, and dynamic batch +dimension (1, 16, 64). TestRandomForestExport validates +file creation and ONNX inference returning class predictions +and probabilities. TestIsolationForestExport validates file +creation and ONNX anomaly scores matching sklearn +decision_function within 1e-4 tolerance + +Connects to: + ml/export_onnx - export_autoencoder, + export_random_forest, + export_isolation_forest + ml/autoencoder - ThreatAutoencoder for AE export """ from pathlib import Path diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_features.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_features.py index 34fee45e..9771ee33 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_features.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_features.py @@ -2,7 +2,31 @@ ©AngelaMos | 2026 test_features.py -Tests per-request feature extraction, Redis sliding-window aggregation, and feature encoding. +Tests the 23 per-request feature extractor, Redis sliding- +window aggregator (12 windowed features), and 35-dim +feature encoder + +Validates all 23 feature keys are returned, path_depth +counts segments, path_entropy distinguishes random vs +simple paths, query param count and length, percent- +encoding and double-encoding detection, status class +grouping, temporal features (hour, day, weekend), bot +and scanner UA detection, attack pattern detection (SQLi, +XSS, traversal), special char ratio, private IP, file +extension, and country code passthrough. WindowAggregator +tests use fakeredis to validate single/multi-request +counts, error rate calculation, unique paths/UAs, TTL +setting, and window boundary exclusion. Encoder tests +validate 35-element output, method/status ordinal mapping, +boolean-to-float, numerical passthrough, and unknown +categorical fallback + +Connects to: + core/features/extractor - extract_request_features + core/features/aggregator - WindowAggregator + core/features/encoder - encode_for_inference + core/features/mappings - FEATURE_ORDER, METHOD_MAP, + STATUS_CLASS_MAP """ import time diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_geoip.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_geoip.py index 30a6d080..df9cb7f3 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_geoip.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_geoip.py @@ -2,7 +2,19 @@ ©AngelaMos | 2026 test_geoip.py -Tests the GeoIP lookup service including private IP handling and missing database fallback. +Tests the GeoIPService MaxMind lookup including private IP +handling, error cases, and missing database fallback + +Validates GeoResult field storage, successful lookup +returning country/city/lat/lon, private and loopback IPs +returning None without hitting the reader, AddressNotFound +Error returning None, None reader returning None, missing +city name handled gracefully, non-existent .mmdb path sets +reader to None, and valid .mmdb path opens the reader via +mock + +Connects to: + core/enrichment/geoip - GeoIPService, GeoResult """ from unittest.mock import MagicMock, patch diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_inference.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_inference.py index 91baf558..a407dea9 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_inference.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_inference.py @@ -2,7 +2,23 @@ ©AngelaMos | 2026 test_inference.py -Tests the InferenceEngine: model loading, predict output shapes, score ranges, and missing-model handling. +Tests the ONNX InferenceEngine for model loading, batch +prediction, score ranges, and error handling + +Uses a model_dir fixture with all 3 exported ONNX models, +scaler.json, and threshold.json. Validates is_loaded=True +with all models, is_loaded=False for nonexistent and +partial directories, predict returns None when not loaded, +predict returns ae/rf/if score dicts, AE scores are non- +negative, RF probabilities are in [0, 1], single-sample +prediction works, threshold loads from JSON, and partial +model sets (AE only) report not loaded + +Connects to: + core/detection/inference - InferenceEngine + ml/export_onnx - model export for fixture + ml/scaler - FeatureScaler for fixture + ml/autoencoder - ThreatAutoencoder for fixture """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_integration.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_integration.py index 1fc612ce..b461a1b1 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_integration.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_integration.py @@ -2,7 +2,24 @@ ©AngelaMos | 2026 test_integration.py -End-to-end tests covering the full path from log file write through tailer, pipeline, and database storage. +End-to-end tests covering the full path from log file +write through tailer, pipeline, and database storage + +integration_env fixture creates a temp log file, in-memory +SQLite, fake Redis, AlertDispatcher, RuleEngine, Pipeline, +and LogTailer wired together. Tests write nginx-format log +lines (normal, SQLi, XSS, path traversal) to the file and +poll the database for stored ThreatEvent rows. Validates +that MEDIUM+ threats are persisted, LOW severity requests +are not stored, and stored events have correct severity, +score, matched_rules, feature_vector length, and source_ip + +Connects to: + core/ingestion/tailer - LogTailer + core/ingestion/pipeline - Pipeline + core/alerts/dispatcher - AlertDispatcher + core/detection/rules - RuleEngine + models/threat_event - ThreatEvent """ import asyncio diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_metadata.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_metadata.py index d205436d..f513dace 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_metadata.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_metadata.py @@ -1,6 +1,24 @@ """ ©AngelaMos | 2026 test_metadata.py + +Tests SHA-256 model version hashing and async metadata +persistence to the database + +TestComputeModelVersion verifies 12-char hex output, +deterministic hashing (same file = same version), and +distinct versions for different files. TestSaveModel +Metadata uses an in-memory SQLite session and fake ONNX +artifacts to validate 3-row creation (one per model type), +is_active flag on new rows, correct model_type values +(autoencoder, random_forest, isolation_forest), previous +active row deactivation on re-save, and inactive row +preservation (6 total rows after two saves) + +Connects to: + ml/metadata - compute_model_version, + save_model_metadata + models/model_metadata - ModelMetadata ORM model """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ml_integration.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ml_integration.py index 3e4a2af8..dd80b464 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ml_integration.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_ml_integration.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 test_ml_integration.py + +Tests the ML inference engine wired into the ingestion +pipeline in hybrid detection mode + +Uses a trained_model_dir fixture with ONNX models to build +a pipeline with InferenceEngine. Validates hybrid detection +mode is set when ML models are present, final_score is in +[0, 1], rules-only mode falls back to rule score as final +score, attack lines score higher than benign in hybrid +mode, and rule_result is preserved alongside ML scores + +Connects to: + core/detection/inference - InferenceEngine + core/detection/rules - RuleEngine + core/ingestion/pipeline - Pipeline, ScoredRequest + ml/export_onnx - model export for fixture """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_orchestrator.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_orchestrator.py index 682d5367..39937891 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_orchestrator.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_orchestrator.py @@ -1,6 +1,20 @@ """ ©AngelaMos | 2026 test_orchestrator.py + +Tests the TrainingOrchestrator pipeline from data splitting +through model export, validation, and MLflow logging + +Verifies all 5 output files are produced (ae.onnx, rf.onnx, +if.onnx, scaler.json, threshold.json), TrainingResult +dataclass structure, scaler.json keys (center, scale, +n_features), threshold.json float value, per-model metrics +presence (ae_threshold, rf f1, if n_samples), ensemble +validation metrics, MLflow run ID capture (32-char hex), +and passed_gates boolean type + +Connects to: + ml/orchestrator - TrainingOrchestrator, TrainingResult """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_parsers.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_parsers.py index 01a2639b..542ac560 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_parsers.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_parsers.py @@ -2,7 +2,19 @@ ©AngelaMos | 2026 test_parsers.py -Tests nginx combined log line parsing via parse_combined. +Tests nginx combined-format log line parsing via the +parse_combined function + +Validates full field extraction (IP, timestamp, method, +path, query string, status code, response size, referer, +user agent, raw line), IPv4 and IPv6 address handling, +dash-referer normalization to empty string, multi-parameter +query strings with special characters, malformed and empty +line None returns, dash response size normalization to +zero, and full-length IPv6 address parsing + +Connects to: + core/ingestion/parsers - parse_combined, ParsedLogEntry """ from datetime import datetime, UTC diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_pipeline.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_pipeline.py index 5befebb3..3171494d 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_pipeline.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_pipeline.py @@ -2,7 +2,21 @@ ©AngelaMos | 2026 test_pipeline.py -Tests the async ingestion pipeline: parsing, feature extraction, rule scoring, and shutdown. +Tests the async ingestion pipeline across all 4 stages: +parsing, feature extraction, rule scoring, and dispatch + +Uses a fakeredis-backed Pipeline with a results collector +callback. Validates that valid log lines flow end-to-end +producing a ScoredRequest with correct IP, method, 35-dim +feature vector, and LOW severity. Confirms malformed lines +are dropped without crashing, backpressure works with +maxsize=1 queues, stop() drains remaining items with all +tasks completing cleanly, and SQLi payloads score HIGH with +SQL_INJECTION rule match + +Connects to: + core/ingestion/pipeline - Pipeline, ScoredRequest + core/detection/rules - RuleEngine """ import fakeredis.aioredis diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_scaler.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_scaler.py index 2125455a..2631fc74 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_scaler.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_scaler.py @@ -2,7 +2,21 @@ ©AngelaMos | 2026 test_scaler.py -Tests the FeatureScaler: fitting, transform correctness, JSON serialization, and round-trip loading. +Tests the FeatureScaler IQR-based normalization for +fitting, transform correctness, JSON round-trip, and error +handling + +Validates n_features is stored after fit, transform +preserves shape and float32 dtype, median of scaled +features is near zero, inverse_transform recovers original +values within 1e-5, save_json creates a valid JSON file +with center/scale/n_features keys, load_json round-trip +produces identical transform output within 1e-6, transform +before fit raises RuntimeError, and fit_transform +convenience method works + +Connects to: + ml/scaler - FeatureScaler """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_splitting.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_splitting.py index 73d17644..097b80d0 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_splitting.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_splitting.py @@ -1,6 +1,20 @@ """ ©AngelaMos | 2026 test_splitting.py + +Tests stratified train/val/test splitting with SMOTE +oversampling for imbalanced datasets + +Validates TrainingSplit dataclass return, 70/15/15 split +proportions within tolerance, stratified class distribution +preservation in val/test sets, SMOTE minority ratio near +target strategy (0.3), val/test sizes unaffected by SMOTE, +X_normal_train containing only class-0 rows, small dataset +(50 samples) success, single-class ValueError, and SMOTE +skip when minority count is below k_neighbors threshold + +Connects to: + ml/splitting - prepare_training_data, TrainingSplit """ import numpy as np diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_synthetic.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_synthetic.py index c4fc48c1..5973fbb9 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_synthetic.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_synthetic.py @@ -1,6 +1,24 @@ """ ©AngelaMos | 2026 test_synthetic.py + +Tests synthetic HTTP traffic generators and mixed dataset +assembly for ML training + +TestGenerators validates all 7 per-type generators (SQLi, +XSS, traversal, Log4Shell, SSRF, scanner, normal) return +correct counts, contain expected payload patterns (OR/UNION +for SQLi, script/alert for XSS, ../ for traversal), return +ParsedLogEntry instances, and pass through feature +extraction and encoding to 35-dim vectors. TestMixedDataset +verifies correct X shape (n, 35), dual-label y, matching +label counts, and finite feature values + +Connects to: + ml/synthetic - all generate_* functions, + generate_mixed_dataset + core/features/extractor - extract_request_features + core/features/encoder - encode_for_inference """ import numpy as np diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training.py index a5d32b14..ac8d4b1c 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training.py @@ -2,7 +2,24 @@ ©AngelaMos | 2026 test_training.py -Tests training pipelines for the autoencoder, random forest, and isolation forest models. +Tests training functions for the autoencoder, random forest, +and isolation forest models + +TestAutoencoderTraining validates train_autoencoder returns +model/threshold/scaler/history, threshold is positive, +history has correct epoch count, higher percentile yields +higher threshold, and returned model is in eval mode. +TestRandomForestTraining validates model/metrics return, +predict_proba availability, required metric keys (f1, +pr_auc, accuracy, precision, recall), probability range, +and metric value range. TestIsolationForestTraining +validates model return, score_samples availability, +n_samples metric, and normal/outlier score separation + +Connects to: + ml/train_autoencoder - train_autoencoder + ml/train_classifiers - train_random_forest, + train_isolation_forest """ import numpy as np diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py index 36465b41..c63b21f7 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_training_e2e.py @@ -1,6 +1,24 @@ """ ©AngelaMos | 2026 test_training_e2e.py + +End-to-end training integration test from synthetic data +generation through ONNX inference and score fusion + +test_full_training_produces_loadable_models generates a +500-normal/200-attack synthetic dataset, runs the full +TrainingOrchestrator pipeline with 3 epochs, verifies all +5 output files (ae.onnx, rf.onnx, if.onnx, scaler.json, +threshold.json), loads models via InferenceEngine, runs +batch prediction, normalizes and fuses per-model scores, +blends with rule scores, and asserts all values are in +[0, 1]. Validates passed_gates is a boolean + +Connects to: + ml/orchestrator - TrainingOrchestrator + ml/synthetic - generate_mixed_dataset + core/detection/ensemble - normalize, fuse, blend + core/detection/inference - InferenceEngine """ from pathlib import Path diff --git a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_validation.py b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_validation.py index 9bff193f..0eaed56a 100644 --- a/PROJECTS/advanced/ai-threat-detection/backend/tests/test_validation.py +++ b/PROJECTS/advanced/ai-threat-detection/backend/tests/test_validation.py @@ -1,6 +1,22 @@ """ ©AngelaMos | 2026 test_validation.py + +Tests post-training ensemble validation with quality gates + +Uses a trained_model_dir fixture with all 3 ONNX models, +scaler, and threshold, plus a separable_test_data fixture +with well-separated normal/attack clusters. Validates +ValidationResult structure, metric ranges (precision, +recall, f1, pr_auc, roc_auc all in [0, 1]), 2x2 confusion +matrix shape, gate_details keys (pr_auc, f1), gate pass +with low thresholds, gate fail with high thresholds, and +custom ensemble weight acceptance + +Connects to: + ml/validation - validate_ensemble, ValidationResult + ml/export_onnx - model export for fixture setup + ml/scaler - FeatureScaler for fixture setup """ import json diff --git a/PROJECTS/advanced/ai-threat-detection/compose.yml b/PROJECTS/advanced/ai-threat-detection/compose.yml index e114396d..86a1d593 100644 --- a/PROJECTS/advanced/ai-threat-detection/compose.yml +++ b/PROJECTS/advanced/ai-threat-detection/compose.yml @@ -1,5 +1,20 @@ # ©AngelaMos | 2026 -# Production Docker Compose +# compose.yml +# +# Production Docker Compose stack for AngelusVigil +# +# Orchestrates 5 services on the vigil_network bridge: +# postgres (18-alpine with healthcheck and persistent +# volume), redis (7.4-alpine with custom redis.conf), +# backend (FastAPI with asyncpg, Redis, nginx log tail, +# GeoIP, and model data volumes), frontend (Vite +# production build served via nginx on the host port), +# and geoip-updater (MaxMind weekly refresh). Joins the +# external certgames_net network and mounts the external +# certgames_nginx_logs volume for real-time log access. +# Connects to infra/docker/fastapi.prod, +# infra/docker/vite.prod, infra/redis/redis.conf, +# infra/nginx/vigil.conf services: postgres: diff --git a/PROJECTS/advanced/ai-threat-detection/dev-log/Dockerfile b/PROJECTS/advanced/ai-threat-detection/dev-log/Dockerfile index b7674904..96aab5d2 100644 --- a/PROJECTS/advanced/ai-threat-detection/dev-log/Dockerfile +++ b/PROJECTS/advanced/ai-threat-detection/dev-log/Dockerfile @@ -1,5 +1,12 @@ # ©AngelaMos | 2026 # Dockerfile +# +# Container image for the dev-log FastAPI target application +# +# Based on python:3.14-slim with uv copied from the official +# astral-sh image. Installs fastapi and uvicorn system-wide, +# copies app.py, and runs uvicorn on port 8000. Sits behind +# the nginx reverse proxy defined in compose.yml FROM python:3.14-slim diff --git a/PROJECTS/advanced/ai-threat-detection/dev-log/app.py b/PROJECTS/advanced/ai-threat-detection/dev-log/app.py index 3850b1f6..3ccf2e9e 100644 --- a/PROJECTS/advanced/ai-threat-detection/dev-log/app.py +++ b/PROJECTS/advanced/ai-threat-detection/dev-log/app.py @@ -1,6 +1,25 @@ """ ©AngelaMos | 2026 app.py + +Minimal FastAPI target application for generating nginx +access logs during development + +Exposes realistic REST endpoints that the simulate.py +traffic generator hits through the nginx reverse proxy: +/ (HTML landing), /health, /api/users (list and by ID), +/api/login (POST returning a fake JWT), /api/search with +query parameter, /api/products (list and by ID), +/api/checkout (POST), /admin and /admin/dashboard (403 +forbidden), and /static/{path} (404). Designed to produce +diverse nginx combined-format log lines for testing the +ingestion pipeline and rule engine + +Connects to: + dev-log/nginx.conf - proxied behind nginx + dev-log/simulate.py - traffic generator targets these + endpoints + dev-log/compose.yml - containerized as vigil-devlog-app """ from fastapi import FastAPI, Request diff --git a/PROJECTS/advanced/ai-threat-detection/dev-log/compose.yml b/PROJECTS/advanced/ai-threat-detection/dev-log/compose.yml index 98d26ea0..bd2c89af 100644 --- a/PROJECTS/advanced/ai-threat-detection/dev-log/compose.yml +++ b/PROJECTS/advanced/ai-threat-detection/dev-log/compose.yml @@ -1,5 +1,15 @@ # ©AngelaMos | 2026 # compose.yml +# +# Docker Compose stack for the dev-log traffic generation +# environment +# +# Runs two services on a bridge network: app (FastAPI target +# built from the local Dockerfile with a /health check) and +# nginx (alpine image reverse-proxying port 58319 to the +# app, writing combined-format access logs to a named volume +# vigil_dev_nginx_logs). The nginx container clears stale +# log files on startup for clean sessions services: app: diff --git a/PROJECTS/advanced/ai-threat-detection/dev-log/nginx.conf b/PROJECTS/advanced/ai-threat-detection/dev-log/nginx.conf index 7ac766c5..2dfd095e 100644 --- a/PROJECTS/advanced/ai-threat-detection/dev-log/nginx.conf +++ b/PROJECTS/advanced/ai-threat-detection/dev-log/nginx.conf @@ -1,5 +1,15 @@ # ©AngelaMos | 2026 # nginx.conf +# +# Nginx reverse proxy configuration for the dev-log traffic +# generation environment +# +# Listens on port 80, proxies all requests to the upstream +# FastAPI app on port 8000, and writes combined-format +# access logs to /var/log/nginx/access.log. Sets X-Real-IP, +# X-Forwarded-For, and X-Forwarded-Proto headers for the +# backend. The log output is mounted as a named volume in +# compose.yml for consumption by the ingestion pipeline events { worker_connections 64; diff --git a/PROJECTS/advanced/ai-threat-detection/dev-log/simulate.py b/PROJECTS/advanced/ai-threat-detection/dev-log/simulate.py index d054c5c1..db011e32 100755 --- a/PROJECTS/advanced/ai-threat-detection/dev-log/simulate.py +++ b/PROJECTS/advanced/ai-threat-detection/dev-log/simulate.py @@ -2,6 +2,26 @@ """ ©AngelaMos | 2026 simulate.py + +HTTP traffic simulator for generating realistic attack and +normal log patterns against the dev-log target application + +Provides 10 traffic modes via argparse: normal (benign +browsing with GET/POST mix), sqli (12 SQL injection +payloads), xss (10 script/event handler vectors), +traversal (10 dot-dot-slash and encoding variants), cmdi +(7 shell command injection payloads), log4shell (4 JNDI +lookup variants), ssrf (5 cloud metadata and internal +service targets), scanner (20 recon paths with 11 scanner +user-agents), flood (rapid-fire requests), and mixed +(50/10/40 normal/scanner/attack split). Uses urllib for +HTTP requests with configurable count, delay, and target +URL. Checks /health reachability before starting + +Connects to: + dev-log/app.py - target endpoints + dev-log/nginx.conf - requests proxied through nginx + to generate access.log entries """ import argparse diff --git a/PROJECTS/advanced/ai-threat-detection/dev.compose.yml b/PROJECTS/advanced/ai-threat-detection/dev.compose.yml index d51d23b4..3575a9aa 100644 --- a/PROJECTS/advanced/ai-threat-detection/dev.compose.yml +++ b/PROJECTS/advanced/ai-threat-detection/dev.compose.yml @@ -1,5 +1,18 @@ # ©AngelaMos | 2026 # dev.compose.yml +# +# Development Docker Compose stack with exposed ports and +# hot reload +# +# Orchestrates 4 services on the vigil_dev bridge: postgres +# (18-alpine on host port 16969 with default devpassword), +# redis (7.4-alpine on host port 26969 with appendonly), +# backend (FastAPI dev build on host port 36969 with debug +# enabled, quiet gitpython, and SKIP_AUTO_TRAIN toggle), +# and frontend (Vite dev server on host port 46969 with +# source bind-mount for HMR and API proxy to the backend). +# Connects to infra/docker/fastapi.dev, +# infra/docker/vite.dev services: postgres: diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/App.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/App.tsx index efbd5388..cfdc5a6b 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/App.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/App.tsx @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // App.tsx +// +// Root React component with providers and routing +// +// Wraps the application in QueryClientProvider (TanStack +// React Query), provides the browser router via +// RouterProvider, renders a dark-themed Sonner toast +// container at top-right, and includes ReactQueryDevtools +// in development mode // =================== import { QueryClientProvider } from '@tanstack/react-query' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts index 880b1482..06a72379 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/index.ts @@ -1,6 +1,13 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// Barrel export for API query hooks +// +// Re-exports useAlerts (WebSocket alert stream), +// useModelStatus and useRetrain (model management), +// useStats (dashboard statistics), and useThreats and +// useThreat (threat event listing and detail) // =================== export * from './useAlerts' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts index b4a2eb38..392684bf 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useAlerts.ts @@ -1,6 +1,21 @@ // =================== // © AngelaMos | 2026 // useAlerts.ts +// +// WebSocket alert stream hook with Zustand state and +// exponential reconnect +// +// Maintains a Zustand AlertState store holding the alert +// ring buffer (capped at ALERTS.MAX_ITEMS), connection +// status, and error state. useAlerts opens a WebSocket to +// WS_ENDPOINTS.ALERTS, validates incoming JSON frames +// against WebSocketAlertSchema via safeParse, stamps each +// with a crypto.randomUUID id, and prepends to the store. +// On close the hook schedules reconnection with exponential +// backoff (RECONNECT_BASE_MS * 2^attempt, capped at +// RECONNECT_MAX_MS). Cleanup on unmount closes the socket +// and clears the retry timer. Connects to api/types/ +// websocket.types, config, components/alert-feed // =================== import { useEffect, useRef } from 'react' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts index 99796a90..12591e40 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useModels.ts @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // useModels.ts +// +// TanStack Query hooks for model status and retraining +// +// useModelStatus fetches API_ENDPOINTS.MODELS.STATUS and +// validates the response through ModelStatusSchema, using +// the standard query strategy. useRetrain posts to +// API_ENDPOINTS.MODELS.RETRAIN, validates through +// RetrainResponseSchema, shows a Sonner success toast, and +// invalidates all QUERY_KEYS.MODELS queries to refresh the +// status display. Connects to api/types/models.types, +// core/api, config, pages/models // =================== import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts index 10ef089f..e5633aa0 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useStats.ts @@ -1,6 +1,16 @@ // =================== // © AngelaMos | 2026 // useStats.ts +// +// TanStack Query hook for dashboard statistics +// +// useStats accepts an optional time range string (defaults +// to 24h) and queries API_ENDPOINTS.STATS with the range +// as a query parameter. The response is validated through +// StatsResponseSchema and the hook uses the frequent query +// strategy for short stale times and automatic refetch +// intervals. Connects to api/types/stats.types, core/api, +// config, pages/dashboard // =================== import { useQuery } from '@tanstack/react-query' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts index 047a6c3a..6ef969f0 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/hooks/useThreats.ts @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // useThreats.ts +// +// TanStack Query hooks for threat event listing and detail +// +// useThreats accepts optional ThreatParams (limit, offset, +// severity, source_ip, since, until) with PAGINATION +// defaults, queries API_ENDPOINTS.THREATS.LIST, and +// validates through ThreatListSchema using the frequent +// strategy. useThreat fetches a single threat by id from +// API_ENDPOINTS.THREATS.BY_ID, validates through +// ThreatEventSchema, and is disabled when id is null. +// Connects to api/types/threats.types, core/api, config, +// pages/threats // =================== import { useQuery } from '@tanstack/react-query' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/index.ts index 8eb1f51a..0da48c21 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/index.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/index.ts @@ -1,6 +1,13 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// Barrel export for the API layer +// +// Re-exports all TanStack Query hooks (useAlerts, +// useModelStatus, useRetrain, useStats, useThreats, +// useThreat) and Zod-validated type definitions from the +// hooks and types sub-modules // =================== export * from './hooks' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/index.ts index 17e60acc..8854805f 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/index.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/index.ts @@ -1,6 +1,15 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// Barrel export for Zod-validated API type definitions +// +// Re-exports all Zod schemas and inferred TypeScript types +// from models.types (ActiveModel, ModelStatus, +// RetrainResponse), stats.types (SeverityBreakdown, +// IPStatEntry, PathStatEntry, StatsResponse), threats.types +// (GeoInfo, ThreatEvent, ThreatList), and websocket.types +// (WebSocketAlert) // =================== export * from './models.types' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/models.types.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/models.types.ts index 2c0bc055..029974b3 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/models.types.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/models.types.ts @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // models.types.ts +// +// Zod schemas and types for ML model status and retraining +// +// Defines ActiveModelSchema with model_type, version, +// training_samples, a flexible metrics record, and nullable +// threshold. ModelStatusSchema wraps models_loaded flag, +// detection_mode string, and an array of ActiveModel +// entries. RetrainResponseSchema captures the status and +// job_id returned when a retrain is triggered. All types +// are inferred from their schemas via z.infer. Connects to +// api/hooks/useModels, pages/models // =================== import { z } from 'zod' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/stats.types.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/stats.types.ts index bb8b8985..a16c889e 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/stats.types.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/stats.types.ts @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // stats.types.ts +// +// Zod schemas and types for dashboard statistics +// +// Defines SeverityBreakdownSchema with high/medium/low +// integer counts, IPStatEntrySchema and PathStatEntrySchema +// for ranked lists with source_ip or path plus count, and +// StatsResponseSchema combining time_range, +// threats_stored, threats_detected, severity_breakdown, +// top_source_ips, and top_attacked_paths. All types are +// inferred from their schemas via z.infer. Connects to +// api/hooks/useStats, pages/dashboard // =================== import { z } from 'zod' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/threats.types.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/threats.types.ts index 01825d15..cc445c0c 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/threats.types.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/threats.types.ts @@ -1,6 +1,19 @@ // =================== // © AngelaMos | 2026 // threats.types.ts +// +// Zod schemas and types for threat event data +// +// Defines GeoInfoSchema with nullable country, city, lat, +// and lon fields. ThreatEventSchema captures the full +// threat record: uuid id, timestamps, source_ip, HTTP +// request details, threat_score, severity enum (HIGH, +// MEDIUM, LOW), per-model component_scores record, geo +// info, nullable matched_rules array, model_version, +// reviewed flag, and review_label. ThreatListSchema wraps +// paginated results with total, limit, offset, and items +// array. Connects to api/hooks/useThreats, pages/threats, +// components/threat-detail // =================== import { z } from 'zod' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/websocket.types.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/websocket.types.ts index d07f9564..8a70a6a4 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/websocket.types.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/api/types/websocket.types.ts @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // websocket.types.ts +// +// Zod schema and type for real-time WebSocket alert frames +// +// Defines WebSocketAlertSchema validating incoming JSON +// frames from the alert WebSocket: optional id (stamped +// client-side), literal 'threat' event discriminator, +// timestamp, source_ip, request_method (defaults to GET), +// request_path, threat_score, severity string, and +// per-model component_scores record. The WebSocketAlert +// type is inferred via z.infer. Connects to +// api/hooks/useAlerts, components/alert-feed // =================== import { z } from 'zod' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.module.scss index bc2739b4..09f904ed 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.module.scss @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // alert-feed.module.scss +// +// CSS module for the live alert feed component +// +// Styles the feed container with surface background and +// muted border, a header row with title and connection +// status dot (green connected, red disconnected), and a +// scrollable list of alert rows in a 6-column grid +// (time, IP, method, path, severity, score). Applies +// tabular-nums to time and score columns, monospace to +// IP, truncation to path, and per-method color classes +// (methodGet through methodOptions) using $method-* tokens. +// Connects to components/alert-feed, styles/_tokens // =================== @use '@/styles/tokens' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.tsx index 4ef8b0c4..eef1904a 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/alert-feed.tsx @@ -1,6 +1,21 @@ // =================== // © AngelaMos | 2026 // alert-feed.tsx +// +// Live WebSocket alert feed with auto-scroll and color-coded +// entries +// +// Renders a scrollable list of WebSocketAlert items with a +// connection status indicator dot (green connected, red +// disconnected). Each row displays formatted timestamp, +// source IP, HTTP method with color-coded SCSS class +// (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS), request path, +// SeverityBadge, and threat score. Auto-scrolls to the top +// on new alerts via a useEffect keyed on alert count. Shows +// an empty-state message when no alerts are present. +// Connects to api/types/websocket.types, +// components/severity-badge, api/hooks/useAlerts, +// pages/dashboard // =================== import { useEffect, useRef } from 'react' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx index 07d23849..1bff3b4e 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/index.tsx @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // index.tsx +// +// Barrel export for shared UI components +// +// Re-exports AlertFeed (live WebSocket alert stream), +// MethodBadge (color-coded HTTP method label), +// SeverityBadge (HIGH/MEDIUM/LOW indicator), StatCard +// (metric display with label and sublabel), and +// ThreatDetail (modal dialog for full threat inspection) // =================== export { AlertFeed } from './alert-feed' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.module.scss index d0d6d48f..04f45201 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.module.scss @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // method-badge.module.scss +// +// CSS module for the HTTP method badge component +// +// Styles a monospace semibold 2xs text badge with wide +// letter spacing and a default $text-lighter color. Seven +// method variant classes (.get through .options) apply +// $method-* token colors. Connects to +// components/method-badge, styles/_tokens // =================== @use '@/styles/tokens' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.tsx index 10be6ff8..febcb828 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/method-badge.tsx @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // method-badge.tsx +// +// Color-coded HTTP method badge component +// +// Renders a span with a base badge class and an +// additional SCSS module class mapped from the method +// string (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) +// via the METHOD_STYLES record. Unknown methods receive +// only the base style. Connects to pages/threats // =================== import styles from './method-badge.module.scss' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.module.scss index 338d2b01..d1cc952e 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.module.scss @@ -1,6 +1,16 @@ // =================== // © AngelaMos | 2026 // severity-badge.module.scss +// +// CSS module for the severity badge component +// +// Styles a compact inline-flex pill badge with full border +// radius, 2xs semibold uppercase text, and wider letter +// spacing. Three severity variants map to token pairs: +// .high ($severity-high text on $severity-high-bg), .medium +// ($severity-medium on $severity-medium-bg), and .low +// ($severity-low on $severity-low-bg). Connects to +// components/severity-badge, styles/_tokens // =================== @use '@/styles/tokens' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.tsx index 2389db84..3b883ae5 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/severity-badge.tsx @@ -1,6 +1,15 @@ // =================== // © AngelaMos | 2026 // severity-badge.tsx +// +// Threat severity level badge component +// +// Renders a span with a base badge class and a +// severity-specific SCSS module class derived by +// lowercasing the severity prop (HIGH, MEDIUM, LOW). +// Used across the alert feed, threats table, and threat +// detail modal. Connects to components/alert-feed, +// components/threat-detail, pages/threats, pages/dashboard // =================== import styles from './severity-badge.module.scss' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.module.scss index 33b01cc7..e7557319 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.module.scss @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // stat-card.module.scss +// +// CSS module for the dashboard metric card component +// +// Styles a column flex card with surface background, muted +// border, and large border radius. Displays a 3xl semibold +// value with tight letter spacing, an sm lighter label, and +// an optional xs muted sublabel. Connects to +// components/stat-card, styles/_tokens // =================== @use '@/styles/tokens' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.tsx index cfd5da7e..8b06ffa8 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/stat-card.tsx @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // stat-card.tsx +// +// Dashboard metric card component +// +// Renders a card displaying a prominent value (string or +// number), a descriptive label underneath, and an optional +// sublabel for secondary context. Used on the dashboard +// page to show threat counts, detection rates, and time +// range indicators. Connects to pages/dashboard // =================== import styles from './stat-card.module.scss' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.module.scss index d6145de2..c3f991ae 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.module.scss @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // threat-detail.module.scss +// +// CSS module for the threat detail slide-in panel +// +// Styles a full-height modal overlay (60% opacity black, +// z-modal) with a right-aligned 520px max-width panel. +// Contains a sticky header with close button, sectioned +// body with 2-column grids for overview and request fields, +// a component scores section with labeled progress bars +// ($accent fill on $bg-surface-300 track), and a matched +// rules section with monospace accent-tinted tags. Connects +// to components/threat-detail, styles/_tokens // =================== @use '@/styles/tokens' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.tsx index 83c7bc5b..ef79d97b 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/components/threat-detail.tsx @@ -1,6 +1,19 @@ // =================== // © AngelaMos | 2026 // threat-detail.tsx +// +// Modal dialog for full threat event inspection +// +// Renders a click-to-dismiss overlay with a detail panel +// displaying four sections: Overview (severity badge, +// threat score to 4 decimals, detection timestamp, review +// status), Request (source IP, method, path, status code, +// response size, user agent), Component Scores (per-model +// score bars with percentage fill widths), and conditionally +// Geolocation (country, city) and Matched Rules (tag list). +// Returns null when threat prop is null. Connects to +// api/types/threats.types, components/severity-badge, +// pages/threats // =================== import { LuX } from 'react-icons/lu' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/config.ts index 5ed0f050..dc40e75d 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/config.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/config.ts @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // config.ts +// +// Application-wide constants and configuration values +// +// Centralizes all API endpoint paths (health, ready, +// threats CRUD, stats, model status/retrain), WebSocket +// endpoint (/ws/alerts), TanStack Query cache keys with +// hierarchical namespacing, client-side route paths +// (dashboard, threats, models), localStorage key for UI +// persistence, query timing config (stale, GC, retry), +// pagination defaults (50/100 limit), and alert feed +// settings (max 50 items, exponential reconnect 1s-30s) // =================== export const API_ENDPOINTS = { diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/api.config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/api.config.ts index 162ae2db..1a98c999 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/api.config.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/api.config.ts @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // api.config.ts +// +// Axios HTTP client singleton with error interceptor +// +// Creates an axios instance with base URL from VITE_API_URL +// env var (defaulting to /api), 15-second timeout, and JSON +// content type. Response interceptor transforms AxiosError +// into typed ApiError via transformAxiosError for consistent +// error handling across all API hooks // =================== import axios, { type AxiosError, type AxiosInstance } from 'axios' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/errors.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/errors.ts index a838ea42..05a2e831 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/errors.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/errors.ts @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // errors.ts +// +// Typed API error handling with status code mapping and +// user-facing messages +// +// Defines ApiErrorCode literal union (NETWORK_ERROR, +// VALIDATION_ERROR, NOT_FOUND, CONFLICT, RATE_LIMITED, +// SERVER_ERROR, UNKNOWN_ERROR), ApiError class with code, +// statusCode, details, and getUserMessage() for toast +// display, and transformAxiosError which maps HTTP status +// codes to ApiErrorCode and extracts detail/message from +// FastAPI error responses. Registers ApiError as the +// TanStack React Query default error type // =================== import type { AxiosError } from 'axios' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/index.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/index.ts index f6dc3639..816c59f0 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/index.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/index.ts @@ -1,6 +1,12 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// Barrel export for the core API module +// +// Re-exports apiClient from api.config, ApiError and +// transformAxiosError from errors, and queryClient with +// QUERY_STRATEGIES from query.config // =================== export * from './api.config' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/query.config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/query.config.ts index 9133f9f9..00469d6a 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/query.config.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/api/query.config.ts @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // query.config.ts +// +// TanStack React Query client configuration with retry +// logic and global error toasts +// +// Configures QueryClient with smart retry (skips NOT_FOUND +// and VALIDATION_ERROR, retries up to 3 times with +// exponential backoff capped at 30s), window focus refetch, +// and reconnect refetch. QueryCache shows toast errors only +// for background updates (stale data present). MutationCache +// shows toast errors only when no per-mutation onError is +// set. Exports QUERY_STRATEGIES (standard, frequent, static) +// with tuned stale/gc/refetch settings // =================== import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/routers.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/routers.tsx index bfadf9be..1103c7a0 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/routers.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/routers.tsx @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // routers.tsx +// +// Browser router with lazy-loaded page routes under the +// Shell layout +// +// Defines a createBrowserRouter with Shell as the root +// layout element containing 3 lazy-loaded child routes: +// dashboard (/), threats (/threats), and models (/models). +// Unknown paths redirect to the dashboard via Navigate // =================== import { createBrowserRouter, Navigate, type RouteObject } from 'react-router-dom' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss index 47f0f1f5..1fc56c64 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.module.scss @@ -1,6 +1,20 @@ // =================== // © AngelaMos | 2026 // shell.module.scss +// +// Application shell layout styles with collapsible sidebar +// and responsive mobile drawer +// +// Styles a fixed-position sidebar (240px expanded, 64px +// collapsed) with dot-grid background, NavLink items with +// hover and active states, red-tinted nav icons, a chevron +// collapse toggle hidden on mobile, and a slide-in mobile +// drawer with overlay dismiss. The main content area adjusts +// margin-left to match sidebar width, contains a sticky +// header with dot-grid background and mobile hamburger +// button, and a scrollable content region with radial-dot +// background pattern. Includes error and loading fallback +// states. Connects to core/app/shell, styles/_index // =================== @use '@/styles' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.tsx index 85c2f031..9820facb 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/shell.tsx @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // shell.tsx +// +// Root application shell with sidebar navigation, header, +// and content outlet +// +// Renders a collapsible sidebar with NavLink items +// (Dashboard, Threats, Models), a mobile hamburger menu +// toggle with overlay dismiss, a header showing the current +// page title, and a main content area wrapped in +// ErrorBoundary and Suspense. Sidebar collapsed state +// persists via the Zustand UIStore. ShellErrorFallback +// displays caught errors and ShellLoading shows a loading +// placeholder during lazy route resolution // =================== import { Suspense } from 'react' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/toast.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/toast.module.scss index 29b3f614..a61950a9 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/toast.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/core/app/toast.module.scss @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // toast.module.scss +// +// Sonner toast notification theme overrides +// +// Applies global dark theme CSS custom properties to +// [data-sonner-toaster] for normal, success, error, +// warning, and info variants using $bg-surface-100, +// $border-default, and $text-default tokens. Styles +// [data-sonner-toast] with $radius-md border radius, +// medium-weight titles, light-colored descriptions, and +// hover-brightened close buttons. Error toasts receive a +// $error-default border accent. Connects to App.tsx, +// styles/_index // =================== @use '@/styles' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/main.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/main.tsx index ac319510..3a23a518 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/main.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/main.tsx @@ -1,6 +1,12 @@ // =========================== // ©AngelaMos | 2026 // main.tsx +// +// React application entry point +// +// Mounts the App component inside React.StrictMode onto the +// #root DOM element via createRoot. Imports the global SCSS +// stylesheet for Tailwind-free custom theming // =========================== import { StrictMode } from 'react' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss index 60cf8d62..89e295f4 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/dashboard.module.scss @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // dashboard.module.scss +// +// CSS module for the dashboard page layout +// +// Styles a padded column flex page with a 4-column +// responsive stat row (collapsing to 2 then 1 at lg/sm +// breakpoints), a severity section with a proportional +// colored bar (high/medium/low segments with animated +// widths) and dot legend, a 2-column bottom row +// (collapsing to 1 at lg) for the alert feed and ranked +// lists with monospace labels and tabular-nums counts, and +// a severity-high-themed WebSocket error banner. Connects +// to pages/dashboard, styles/_index // =================== @use '@/styles' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx index a0d371e9..aaf6c333 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/dashboard/index.tsx @@ -1,6 +1,21 @@ // =================== // © AngelaMos | 2026 // index.tsx +// +// Dashboard page with stats overview, severity distribution, +// alert feed, and ranked lists +// +// Exports a lazy-loaded Component (displayName +// DashboardPage) that fetches useStats, useModelStatus, and +// useAlerts. Renders a stat row (threats detected, stored, +// high severity count, detection mode), a proportional +// SeverityBar with colored segments and a SeverityLegend, +// a live AlertFeed capped at 360px, and two RankedList +// panels for top source IPs and most attacked paths. Shows +// a WebSocket connection error banner when present. Connects +// to api/hooks/useStats, api/hooks/useModels, +// api/hooks/useAlerts, components/alert-feed, +// components/stat-card // =================== import { useAlerts, useModelStatus, useStats } from '@/api/hooks' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx index f0106c99..46c42bae 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/index.tsx @@ -1,6 +1,20 @@ // =================== // © AngelaMos | 2026 // index.tsx +// +// Models page with status banner, retrain button, and model +// cards +// +// Exports a lazy-loaded Component (displayName ModelsPage) +// that fetches useModelStatus and provides useRetrain. Shows +// a status banner indicating whether models are loaded and +// the current detection mode. A retrain button triggers the +// mutation with a spinning icon while pending. ModelCard +// renders each ActiveModel entry with model_type, version, +// training_samples, optional threshold to 4 decimals, and +// a metrics section listing all numeric metric key-value +// pairs. Empty state prompts retraining. Connects to +// api/hooks/useModels, api/types/models.types // =================== import { LuRefreshCw } from 'react-icons/lu' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/models.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/models.module.scss index a2e9eb4b..3f6d899b 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/models.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/models/models.module.scss @@ -1,6 +1,18 @@ // =================== // © AngelaMos | 2026 // models.module.scss +// +// CSS module for the models management page +// +// Styles a status banner with loaded (green) and not-loaded +// (amber) severity-themed variants, a retrain button with +// accent-muted background and spinning icon animation during +// pending state, an empty-state placeholder, and a +// responsive 3-column grid (collapsing to 2/1) of model +// cards showing type, version, training samples, threshold, +// and a metrics section with tabular-nums monospace values +// separated by a muted border. Connects to pages/models, +// styles/_index // =================== @use '@/styles' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx index baaeeff2..cc5ad6fa 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/index.tsx @@ -1,6 +1,22 @@ // =================== // © AngelaMos | 2026 // index.tsx +// +// Threats table page with severity and IP filters, +// pagination, and detail modal +// +// Exports a lazy-loaded Component (displayName ThreatsPage) +// that manages offset, severity filter (ALL/HIGH/MEDIUM/ +// LOW), and source IP text filter as local state. Passes +// these as ThreatParams to useThreats with PAGINATION +// defaults. Renders a responsive table with time, source +// IP, MethodBadge, path, score to 3 decimals, +// SeverityBadge, and status code columns. Rows are +// clickable to open ThreatDetail in a modal. Previous/Next +// pagination controls show the current range and total. +// Connects to api/hooks/useThreats, api/types/threats.types, +// components/method-badge, components/severity-badge, +// components/threat-detail, config // =================== import { useState } from 'react' diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/threats.module.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/threats.module.scss index 8f4a79a4..3c987b86 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/threats.module.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/pages/threats/threats.module.scss @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // threats.module.scss +// +// CSS module for the threats table page +// +// Styles a filter bar with select dropdown and text input +// (accent-bordered on focus), a scrollable table wrapper +// with uppercase column headers, clickable rows with hover +// highlight, monospace and truncated cell variants for IPs +// and paths, tabular-nums score cells, centered loading and +// empty states, and a pagination footer with previous/next +// buttons that dim when disabled. Connects to pages/threats, +// styles/_index // =================== @use '@/styles' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles.scss index e06571ba..69d6b8e5 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles.scss @@ -1,6 +1,16 @@ // =================== // © AngelaMos | 2026 // styles.scss +// +// Global stylesheet entry point with root element setup +// +// Forwards tokens, fonts, and mixins for downstream module +// consumption, applies the CSS reset, and defines #root as +// a full-viewport column flex container with $bg-default +// background. The .app class sets flex: 1, column layout, +// default background, text color, and sans font family. +// Connects to styles/_tokens, styles/_fonts, +// styles/_mixins, styles/_reset, main.tsx // =================== @forward 'styles/tokens'; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_fonts.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_fonts.scss index 10d59dca..3a211d40 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_fonts.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_fonts.scss @@ -1,6 +1,14 @@ // =================== // © AngelaMos | 2026 // _fonts.scss +// +// Font family stack definitions +// +// Defines $font-sans (system UI stack: -apple-system, +// BlinkMacSystemFont, Segoe UI, Inter, Roboto, Helvetica +// Neue, Arial) and $font-mono (ui-monospace, SFMono-Regular, +// SF Mono, Menlo, Consolas, Liberation Mono). Connects to +// styles/_tokens, styles/_index, styles/_reset // =================== @use 'tokens' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_index.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_index.scss index 9d5d028c..506508ee 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_index.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_index.scss @@ -1,6 +1,12 @@ // =================== // © AngelaMos | 2026 // _index.scss +// +// SCSS module forwarding index for the styles package +// +// Forwards tokens, fonts, and mixins so that downstream +// SCSS modules can access the full design system with a +// single @use '@/styles' as * import // =================== @forward 'tokens'; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_mixins.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_mixins.scss index 288b4e67..ed25c9b5 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_mixins.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_mixins.scss @@ -1,6 +1,19 @@ // =================== // © AngelaMos | 2026 // _mixins.scss +// +// Reusable SCSS mixins for layout, typography, transitions, +// and responsiveness +// +// Defines a $breakpoints map (xs through 2xl) with +// breakpoint-up and breakpoint-down media query mixins, +// flex layout helpers (flex-center, flex-between, +// flex-column, flex-column-center), accessibility (sr-only), +// text overflow (truncate, line-clamp), transition presets +// (transition-fast, transition-normal), positioning +// (absolute-fill, absolute-center), hover media query +// guard, and hide-scrollbar. Connects to styles/_tokens, +// styles/_index, all *.module.scss files // =================== @use 'sass:map'; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_reset.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_reset.scss index fb9eca5b..cb8a1e0a 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_reset.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_reset.scss @@ -1,6 +1,21 @@ // =================== // © AngelaMos | 2026 // _reset.scss +// +// Modern CSS reset with accessibility and scrollbar styling +// +// Applies universal box-sizing border-box, zeroes margins +// and padding, sets html font-size 16px with +// text-size-adjust, enables smooth scrolling when +// prefers-reduced-motion allows it, configures body with +// 100dvh min-height and optimized text rendering, resets +// headings with balanced text-wrap, strips list styles and +// link decorations, normalizes form elements, adds +// focus-visible outlines with $border-strong, disables +// animations for prefers-reduced-motion, applies safe-area +// insets, and styles 6px scrollbar thumbs with +// $border-default. Connects to styles/_tokens, +// styles/_fonts, styles.scss // =================== @use 'tokens' as *; diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss index fd804e01..efac199b 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss +++ b/PROJECTS/advanced/ai-threat-detection/frontend/src/styles/_tokens.scss @@ -1,6 +1,21 @@ // =================== // © AngelaMos | 2026 // _tokens.scss +// +// Design token variables for the entire UI system +// +// Defines the full token set consumed by all SCSS modules: +// 8px-base spacing scale ($space-0 through $space-32), +// typography scale (3xs through 5xl), font weights (regular, +// medium, semibold), line heights, letter spacing, dark +// theme color palette (backgrounds, borders, text tiers, +// error, severity high/medium/low with backgrounds, accent, +// HTTP method colors GET through OPTIONS, success), border +// radius scale, z-index layers (hide through max), +// transition durations and easings, responsive breakpoints +// (xs 360px through 2xl 1536px), and container width +// constraints. Connects to styles/_index, styles/_reset, +// styles/_fonts, styles/_mixins, all *.module.scss files // =================== // ============================================================================ diff --git a/PROJECTS/advanced/ai-threat-detection/frontend/vite.config.ts b/PROJECTS/advanced/ai-threat-detection/frontend/vite.config.ts index e18938b5..a8f55007 100644 --- a/PROJECTS/advanced/ai-threat-detection/frontend/vite.config.ts +++ b/PROJECTS/advanced/ai-threat-detection/frontend/vite.config.ts @@ -1,6 +1,19 @@ /** * ©AngelaMos | 2026 * vite.config.ts + * + * Vite build configuration with dev proxy and manual chunk + * splitting + * + * Loads environment from the parent directory via loadEnv, + * resolves VITE_API_TARGET for the dev server proxy + * (/api rewrite and /ws WebSocket passthrough), sets @ path + * alias to src, enables SCSS preprocessing, builds to + * esnext with oxc minification and hidden sourcemaps in + * production, and splits vendor chunks into vendor-react + * (react-dom, react-router), vendor-query (TanStack), and + * vendor-state (zustand). Connects to src/main.tsx, + * src/App.tsx, src/config.ts */ import path from 'node:path' diff --git a/PROJECTS/advanced/ai-threat-detection/infra/docker/entrypoint.sh b/PROJECTS/advanced/ai-threat-detection/infra/docker/entrypoint.sh index 36765d01..0bba0d8e 100755 --- a/PROJECTS/advanced/ai-threat-detection/infra/docker/entrypoint.sh +++ b/PROJECTS/advanced/ai-threat-detection/infra/docker/entrypoint.sh @@ -1,6 +1,18 @@ #!/bin/sh # ©AngelaMos | 2026 # entrypoint.sh +# +# Container entrypoint with automatic ML model training +# +# Cleans up any symlinked nginx log files, then checks +# MODEL_DIR for the required ONNX artifacts (ae.onnx, +# rf.onnx, if.onnx, scaler.json, threshold.json). If all +# models exist, skips training. If SKIP_AUTO_TRAIN is true, +# starts in rules-only mode. Otherwise runs cli.main train +# with 2000 normal and 1000 attack synthetic samples, 100 +# epochs, batch size 256. Executes the CMD arguments via +# exec on completion. Connects to cli/main, compose.yml, +# dev.compose.yml MODEL_DIR="${MODEL_DIR:-data/models}" NGINX_LOG_PATH="${NGINX_LOG_PATH:-/var/log/nginx/access.log}" diff --git a/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.dev b/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.dev index 18bbf480..fac6fb6c 100644 --- a/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.dev +++ b/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.dev @@ -1,5 +1,15 @@ # ©AngelaMos | 2026 # fastapi.dev +# +# Development Dockerfile for the FastAPI backend +# +# Based on python:3.14-slim with uv from the astral-sh +# image. Installs build-essential, libpq-dev, and curl, +# then installs the project with dev and ml extras via uv +# pip install --system -e. Copies the backend source and +# entrypoint script, creates the model data directory, and +# runs uvicorn with --reload for live code changes. +# Connects to dev.compose.yml, entrypoint.sh FROM python:3.14-slim diff --git a/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.prod b/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.prod index 9f9ee4b8..9d324a68 100644 --- a/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.prod +++ b/PROJECTS/advanced/ai-threat-detection/infra/docker/fastapi.prod @@ -1,5 +1,16 @@ # ©AngelaMos | 2026 # fastapi.prod +# +# Multi-stage production Dockerfile for the FastAPI backend +# +# Builder stage: python:3.14-slim with uv, compiles +# requirements from pyproject.toml with ml extras and +# installs to /app/deps. Runtime stage: python:3.14-slim +# with libpq5 and curl, copies the dependency tree to a +# non-root appuser site-packages path, copies backend source +# and entrypoint, creates model directory, and runs uvicorn +# without --reload. Includes a HEALTHCHECK on /health. +# Connects to compose.yml, entrypoint.sh FROM python:3.14-slim AS builder diff --git a/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.dev b/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.dev index 1a39298b..37b05c47 100644 --- a/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.dev +++ b/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.dev @@ -1,6 +1,14 @@ # ©AngelaMos | 2026 -# Development Vite Dockerfile -# Hot reload with volume mounts for source code +# vite.dev +# +# Development Dockerfile for the Vite frontend with hot +# reload +# +# Based on node:24-alpine with corepack-activated pnpm. +# Copies package.json, runs pnpm install, copies the +# frontend source, and starts the Vite dev server on port +# 5173. Source code is bind-mounted from the host in +# dev.compose.yml for HMR. Connects to dev.compose.yml FROM node:24-alpine diff --git a/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.prod b/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.prod index 0400504b..4be580cb 100644 --- a/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.prod +++ b/PROJECTS/advanced/ai-threat-detection/infra/docker/vite.prod @@ -1,6 +1,14 @@ # ©AngelaMos | 2026 -# Production Vite Dockerfile -# Multi-stage build: pnpm build → nginx static serving +# vite.prod +# +# Multi-stage production Dockerfile for the Vite frontend +# +# Builder stage: node:24-alpine with corepack pnpm, runs +# frozen-lockfile install and pnpm build to produce the +# dist output. Runtime stage: nginx:alpine, removes the +# default config, copies vigil.conf and the built assets +# to the nginx html root. Includes a HEALTHCHECK on /health. +# Connects to compose.yml, infra/nginx/vigil.conf FROM node:24-alpine AS builder diff --git a/PROJECTS/advanced/ai-threat-detection/infra/nginx/vigil.conf b/PROJECTS/advanced/ai-threat-detection/infra/nginx/vigil.conf index f7f667f1..06338ded 100644 --- a/PROJECTS/advanced/ai-threat-detection/infra/nginx/vigil.conf +++ b/PROJECTS/advanced/ai-threat-detection/infra/nginx/vigil.conf @@ -1,5 +1,18 @@ # ©AngelaMos | 2026 # vigil.conf +# +# Production nginx reverse proxy and static file server +# +# Defines an upstream to the backend on port 8000. Serves +# the Vite-built SPA from /usr/share/nginx/html with +# try_files fallback to index.html. Proxies /api/ requests +# to the backend (stripping the prefix) with standard +# forwarded headers and 30s read timeout. Proxies /ws/ +# requests with HTTP 1.1 upgrade for WebSocket connections +# and 86400s read timeout. Exposes a /health endpoint +# returning 200. Enables gzip for common types and sets +# 1-year immutable cache headers on static assets. Connects +# to compose.yml, frontend Vite build output upstream vigil_backend { server backend:8000; diff --git a/PROJECTS/advanced/ai-threat-detection/infra/redis/redis.conf b/PROJECTS/advanced/ai-threat-detection/infra/redis/redis.conf index 4a9b4b68..08d4762b 100644 --- a/PROJECTS/advanced/ai-threat-detection/infra/redis/redis.conf +++ b/PROJECTS/advanced/ai-threat-detection/infra/redis/redis.conf @@ -1,5 +1,20 @@ # ©AngelaMos | 2026 # redis.conf +# +# Production Redis 7.4 server configuration with security +# hardening +# +# Binds to all interfaces in protected mode. Disables +# dangerous commands (FLUSHALL, FLUSHDB, CONFIG, SHUTDOWN, +# MONITOR, DEBUG, SLAVEOF, MIGRATE) via rename-command. +# Enables AOF persistence with everysec fsync and RDB +# snapshots at 900/1, 300/10, 60/10000 intervals. Sets +# 2GB maxmemory with allkeys-lru eviction, 10000 max +# clients, 4 I/O threads with read offloading, TCP backlog +# 2048, 300s keepalive, slowlog at 10ms, latency monitor +# at 100ms, active defrag (5-20% thresholds), and lazyfree +# for eviction, expiry, and server-del. Connects to +# compose.yml bind 0.0.0.0 diff --git a/PROJECTS/beginner/firewall-rule-engine/src/analyzer/analyzer_test.v b/PROJECTS/beginner/firewall-rule-engine/src/analyzer/analyzer_test.v index 64363e3d..577122b5 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/analyzer/analyzer_test.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/analyzer/analyzer_test.v @@ -1,5 +1,35 @@ -// ©AngelaMos | 2026 -// analyzer_test.v +/* +©AngelaMos | 2026 +analyzer_test.v + +Tests for conflict detection and optimization analysis + +Tests both conflict.v and optimizer.v functions. Conflict tests cover +shadowed rules (broad ACCEPT before narrow DROP), contradictions +(overlapping criteria with opposing actions), duplicates (identical +criteria and action), and redundant rules (strict subset with same +action). Also verifies that disjoint rules (different protocols, non- +overlapping ports) produce no false positives. Comparison helper tests +exercise matches_overlap, match_is_superset, criteria_equal, +actions_conflict, ports_overlap, ports_is_superset, addr_is_superset, +addrs_overlap, and opt_str_equal with various none/some combinations. +Optimizer tests cover mergeable ports, missing SSH rate limits, missing +conntrack, unreachable rules after catch-all drops, overly permissive +source-less rules on sensitive ports, redundant terminal drops against +chain policy, and CIDR /0 containment. + +Connects to: + analyzer/conflict.v - tests analyze_conflicts, find_shadowed_rules, + find_contradictions, find_duplicates, find_redundant_rules, + matches_overlap, match_is_superset, criteria_equal, + actions_conflict, ports_overlap, ports_is_superset, + addr_is_superset, addrs_overlap, opt_str_equal + analyzer/optimizer.v - tests find_mergeable_ports, find_missing_rate_limits, + find_missing_conntrack, find_unreachable_after_drop, + find_overly_permissive, find_redundant_terminal_drop + models/models.v - uses MatchCriteria, NetworkAddr, PortSpec, Rule, Ruleset, + tests cidr_contains directly +*/ module analyzer diff --git a/PROJECTS/beginner/firewall-rule-engine/src/analyzer/conflict.v b/PROJECTS/beginner/firewall-rule-engine/src/analyzer/conflict.v index da9313d1..fe3e6902 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/analyzer/conflict.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/analyzer/conflict.v @@ -1,5 +1,31 @@ -// ©AngelaMos | 2026 -// conflict.v +/* +©AngelaMos | 2026 +conflict.v + +Conflict detection engine for firewall rulesets + +Walks every chain pairwise comparing rules to find four classes of +problems: shadowed rules (a broader rule with a different action appears +earlier, making the narrower rule unreachable), contradictions (two +rules overlap in traffic but have opposing accept/deny actions), +duplicates (identical criteria and action), and redundant rules (a +strict subset of another rule with the same action). The comparison +logic uses match_is_superset for subset testing and matches_overlap for +partial intersection, both of which recurse through protocol, source +address, destination address, ports, interfaces, and conntrack states. +CIDR containment delegates to models.cidr_contains for the actual +prefix arithmetic. + +Key exports: + analyze_conflicts - Scans a Ruleset and returns all conflict Findings + +Connects to: + models/models.v - imports Rule, Ruleset, MatchCriteria, Finding, Action, + NetworkAddr, PortSpec, cidr_contains, port_range_contains + analyzer/optimizer.v - sibling module, both called from main.v cmd_analyze + main.v - called from cmd_analyze + display/display.v - Findings are rendered by print_findings +*/ module analyzer diff --git a/PROJECTS/beginner/firewall-rule-engine/src/analyzer/optimizer.v b/PROJECTS/beginner/firewall-rule-engine/src/analyzer/optimizer.v index a5195b79..18f5ec3d 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/analyzer/optimizer.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/analyzer/optimizer.v @@ -1,5 +1,31 @@ -// ©AngelaMos | 2026 -// optimizer.v +/* +©AngelaMos | 2026 +optimizer.v + +Optimization and hardening suggestions for firewall rulesets + +Produces advisory Findings that do not indicate bugs but highlight ways +to tighten or simplify a ruleset. find_mergeable_ports groups rules that +differ only in destination port and suggests combining them into a +single multiport rule. suggest_reordering flags high-traffic port rules +(HTTP, HTTPS, DNS) buried deep in a chain where they slow traversal. +find_missing_rate_limits warns when sensitive ports like SSH accept +traffic without rate limiting. find_missing_conntrack checks for an +ESTABLISHED/RELATED rule near the top of each chain. find_overly_permissive +flags sensitive ports (SSH, MySQL, PostgreSQL, Redis) open to any source. +find_redundant_terminal_drop catches explicit drop-all rules that duplicate +the chain default policy. + +Key exports: + suggest_optimizations - Scans a Ruleset and returns optimization Findings + +Connects to: + config/config.v - reads port constants, rate-limit defaults, multiport_max + models/models.v - imports Rule, Ruleset, Finding + analyzer/conflict.v - sibling module, both called from main.v cmd_analyze + main.v - called from cmd_analyze and cmd_optimize + display/display.v - Findings are rendered by print_findings +*/ module analyzer diff --git a/PROJECTS/beginner/firewall-rule-engine/src/config/config.v b/PROJECTS/beginner/firewall-rule-engine/src/config/config.v index a1cfacdf..054a1020 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/config/config.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/config/config.v @@ -1,5 +1,33 @@ -// ©AngelaMos | 2026 -// config.v +/* +©AngelaMos | 2026 +config.v + +Application-wide constants for ports, limits, display, and exit codes + +Centralizes every magic number and string the tool uses. Well-known +ports and service_ports drive the hardened ruleset generator so adding a +new service is a one-line map entry. Rate-limit defaults (ssh_rate_limit, +icmp_rate_limit) match common CIS and NIST hardening baselines. +private_ranges lists RFC 1918 CIDR blocks used for anti-spoofing rules. +Column widths and Unicode symbols control the terminal table layout in +the display module. + +Key exports: + version, app_name - Binary identity + exit_success .. exit_usage_error - Process exit codes + port_ssh .. port_ntp - Well-known port constants + private_ranges - RFC 1918 CIDR blocks for spoofing checks + ssh_rate_limit, icmp_rate_limit - Default rate-limit strings + service_ports - Service name to port number map + col_num .. col_action - Terminal table column widths + sym_check .. sym_bullet - Unicode glyphs for display + +Connects to: + analyzer/optimizer.v - reads port constants, rate-limit defaults, multiport_max + generator/generator.v - reads service_ports, private_ranges, rate-limit strings + display/display.v - reads column widths, Unicode symbols, version + main.v - reads app_name, version, exit codes +*/ module config diff --git a/PROJECTS/beginner/firewall-rule-engine/src/display/display.v b/PROJECTS/beginner/firewall-rule-engine/src/display/display.v index 695cb98c..626e4836 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/display/display.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/display/display.v @@ -1,5 +1,32 @@ -// ©AngelaMos | 2026 -// display.v +/* +©AngelaMos | 2026 +display.v + +Terminal output formatting for rulesets, findings, and diffs + +Handles all user-facing output so the rest of the codebase never calls +println directly for structured data. print_rule_table renders a +fixed-width ASCII table with columns for rule number, chain, protocol, +source, destination, ports, and action. Actions are color-coded green +for ACCEPT, red for DROP/REJECT, yellow for LOG. print_findings groups +analyzer results by severity with colored brackets and includes the +suggestion arrow for each finding. print_diff compares two Rulesets by +building a set of normalized rule strings and showing only-left / +only-right entries, similar to a unified diff. + +Key exports: + print_banner - Renders the boxed FWRULE header with version + print_rule_table - Renders a tabular view of all rules in a Ruleset + print_summary - Shows format, rule count, chains, and policies + print_findings - Renders a list of analyzer Findings with severity counts + print_finding - Renders a single Finding with colored severity tag + print_diff - Side-by-side comparison of two Rulesets + +Connects to: + config/config.v - reads column widths, Unicode symbols, version + models/models.v - imports Rule, Ruleset, Finding, Action, Severity + main.v - called from every cmd_* handler for display +*/ module display diff --git a/PROJECTS/beginner/firewall-rule-engine/src/generator/generator.v b/PROJECTS/beginner/firewall-rule-engine/src/generator/generator.v index 05265656..38b5a74a 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/generator/generator.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/generator/generator.v @@ -1,5 +1,28 @@ -// ©AngelaMos | 2026 -// generator.v +/* +©AngelaMos | 2026 +generator.v + +Hardened ruleset generation and cross-format export + +generate_hardened builds a complete firewall ruleset from scratch using +CIS-aligned defaults: default-deny INPUT/FORWARD, loopback acceptance, +conntrack early in the chain, RFC 1918 anti-spoofing, rate-limited ICMP +and SSH, and a LOG rule before the final drop. Services are resolved +through config.service_ports so DNS gets dual tcp/udp rules and NTP gets +udp-only. export_ruleset converts an existing parsed Ruleset into the +opposite format by serializing each Rule through rule_to_iptables or +rule_to_nftables, preserving table and chain structure including +multi-table layouts (filter + nat). + +Key exports: + generate_hardened - Builds a hardened ruleset string for given services and format + export_ruleset - Converts a Ruleset to iptables or nftables string output + +Connects to: + config/config.v - reads service_ports, private_ranges, rate-limit strings, log prefixes + models/models.v - imports Rule, Ruleset, RuleSource + main.v - called from cmd_harden and cmd_export +*/ module generator diff --git a/PROJECTS/beginner/firewall-rule-engine/src/generator/generator_test.v b/PROJECTS/beginner/firewall-rule-engine/src/generator/generator_test.v index 225371c1..911d4615 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/generator/generator_test.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/generator/generator_test.v @@ -1,5 +1,26 @@ -// ©AngelaMos | 2026 -// generator_test.v +/* +©AngelaMos | 2026 +generator_test.v + +Tests for hardened ruleset generation and cross-format export + +Hardened generation tests verify both iptables and nftables output +formats: default-deny policies, loopback acceptance, conntrack early +in the chain, SSH rate limiting, HTTP/HTTPS service rules, RFC 1918 +anti-spoofing drops, ICMP rate limiting, LOG before final drop, COMMIT +wrapping, DNS dual-protocol (tcp+udp), NTP udp-only, and custom +interface names. Serialization tests cover rule_to_iptables and +rule_to_nftables for TCP port rules, source/destination addresses with +negation, multiport sets, interface matching, and log prefix handling. +Export tests verify round-trip conversion of Rulesets including +multi-table layouts with filter and nat tables, correct chain nesting +inside their parent tables, and empty ruleset edge cases. + +Connects to: + generator/generator.v - tests generate_hardened, export_ruleset, rule_to_iptables, + rule_to_nftables + models/models.v - uses Rule, Ruleset, MatchCriteria, NetworkAddr, PortSpec, Action +*/ module generator diff --git a/PROJECTS/beginner/firewall-rule-engine/src/main.v b/PROJECTS/beginner/firewall-rule-engine/src/main.v index 7cc151a3..0dd83033 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/main.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/main.v @@ -1,5 +1,32 @@ -// ©AngelaMos | 2026 -// main.v +/* +©AngelaMos | 2026 +main.v + +CLI entry point with command dispatch and ruleset loading + +Parses the first positional argument as a subcommand and fans out to +the appropriate handler. load/analyze/optimize/diff read a ruleset file +through load_ruleset, which auto-detects iptables vs nftables format +via parser.detect_format before delegating to the correct parser. +harden and export use V's flag module for option parsing (--services, +--iface, --format). Every command prints through the display module so +output formatting is consistent. + +Key exports: + main - Entry point, dispatches to cmd_* handlers + load_ruleset - Reads a file, auto-detects format, returns a Ruleset + +Connects to: + config/config.v - exit codes, app_name, version, default_services, default_iface + models/models.v - RuleSource for format selection in harden/export + parser/common.v - detect_format for auto-detection + parser/iptables.v - parse_iptables for iptables input + parser/nftables.v - parse_nftables for nftables input + analyzer/conflict.v - analyze_conflicts for the analyze command + analyzer/optimizer.v - suggest_optimizations for analyze/optimize commands + generator/generator.v - generate_hardened, export_ruleset + display/display.v - print_banner, print_summary, print_rule_table, print_findings, print_diff +*/ module main diff --git a/PROJECTS/beginner/firewall-rule-engine/src/models/models.v b/PROJECTS/beginner/firewall-rule-engine/src/models/models.v index 857640a8..a9b1f446 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/models/models.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/models/models.v @@ -1,5 +1,45 @@ -// ©AngelaMos | 2026 -// models.v +/* +©AngelaMos | 2026 +models.v + +Domain types for firewall rule representation and matching + +Every rule parsed from iptables or nftables input lands in the same +unified Rule struct so the analyzer and generator never need to know +which format the original file used. Enums use explicit u8 backing for +compact storage when rulesets grow large. NetworkAddr and PortSpec carry +a negated flag so "! -s 10.0.0.0/8" round-trips cleanly through parse, +analyze, and export. ip_to_u32 and cidr_contains power the superset and +overlap checks in the conflict analyzer by converting dotted-quad +addresses to a single u32 for prefix comparison. + +Key exports: + Protocol - Network protocol enum (tcp, udp, icmp, icmpv6, all, sctp, gre) + Action - Firewall target action (accept, drop, reject, log, NAT variants) + Table - Netfilter table (filter, nat, mangle, raw, security) + ChainType - Built-in chain identifiers plus custom + RuleSource - Discriminates iptables from nftables origin + Severity - Finding severity for analyzer output (info, warning, critical) + ConnState - Bitflag set for conntrack states (new, established, related, invalid) + NetworkAddr - IP address with CIDR prefix length and negation + PortSpec - Single port or port range with negation + MatchCriteria - Full match tuple: protocol, addresses, ports, interfaces, conntrack + Rule - One firewall rule: table, chain, action, criteria, line number, raw text + Finding - Analyzer result: severity, title, description, affected rules, suggestion + Ruleset - Collection of rules with chain default policies + ip_to_u32 - Converts dotted-quad IPv4 string to a 32-bit integer + cidr_contains - Tests whether one CIDR prefix fully contains another + port_range_contains - Tests whether one port range fully contains another + +Connects to: + parser/common.v - imports all enums and structs for parsing + parser/iptables.v - imports Rule, Ruleset, MatchCriteria, NetworkAddr, Action, Table + parser/nftables.v - imports Rule, Ruleset, MatchCriteria, NetworkAddr, Action, Table + analyzer/conflict.v - imports Rule, Ruleset, MatchCriteria, Finding, Action, NetworkAddr, PortSpec + analyzer/optimizer.v - imports Rule, Ruleset, Finding + generator/generator.v - imports Rule, Ruleset, RuleSource + display/display.v - imports Rule, Ruleset, Finding, Action, Severity +*/ module models diff --git a/PROJECTS/beginner/firewall-rule-engine/src/parser/common.v b/PROJECTS/beginner/firewall-rule-engine/src/parser/common.v index b9b71bd0..ba2631b9 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/parser/common.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/parser/common.v @@ -1,5 +1,34 @@ -// ©AngelaMos | 2026 -// common.v +/* +©AngelaMos | 2026 +common.v + +Shared parsing primitives and format auto-detection + +Provides the low-level converters that both iptables.v and nftables.v +call: network addresses with optional CIDR and negation, single ports +and port lists, protocol names and numbers, actions, tables, chain +types, and conntrack state flags. detect_format examines the first +non-blank, non-comment line of a ruleset file to choose between the +iptables and nftables parsers. Protocol parsing accepts both names +("tcp") and IANA numbers ("6") so either style works in rule files. + +Key exports: + parse_network_addr - Parses "!10.0.0.0/8" into a NetworkAddr with negation and CIDR + parse_port_spec - Parses "!1024:65535" into a PortSpec with range and negation + parse_port_list - Splits "80,443,8080" into a []PortSpec + parse_protocol - Converts name or IANA number to Protocol enum + parse_action - Converts target string to Action enum + parse_table - Converts table name to Table enum + parse_chain_type - Maps chain name to ChainType, defaults to .custom + parse_conn_states - Splits "ESTABLISHED,RELATED" into a ConnState bitflag set + detect_format - Auto-detects whether input is iptables or nftables + +Connects to: + models/models.v - imports all enum and struct types + parser/iptables.v - calls every function here during rule parsing + parser/nftables.v - calls parse_network_addr, parse_port_spec, parse_protocol, parse_action, parse_table, parse_chain_type, parse_conn_states + main.v - calls detect_format for auto-detection +*/ module parser diff --git a/PROJECTS/beginner/firewall-rule-engine/src/parser/iptables.v b/PROJECTS/beginner/firewall-rule-engine/src/parser/iptables.v index 2bf64c65..2484a7b1 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/parser/iptables.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/parser/iptables.v @@ -1,5 +1,29 @@ -// ©AngelaMos | 2026 -// iptables.v +/* +©AngelaMos | 2026 +iptables.v + +Parser for iptables-save output format + +Reads the *table / :CHAIN POLICY / -A rule / COMMIT structure that +iptables-save produces. tokenize_iptables splits each rule line into +tokens while respecting single and double quotes so log prefixes like +"DROPPED: " stay intact. parse_iptables_rule walks the token stream +flag by flag (-p, -s, -d, --dport, --state, -j, etc.) building a Rule +struct. The ! negation token is tracked across flag boundaries so +"! -s 10.0.0.0/8" correctly sets NetworkAddr.negated. Chain default +policies (":INPUT DROP [0:0]") are stored in Ruleset.policies. + +Key exports: + parse_iptables - Parses a full iptables-save string into a Ruleset + parse_iptables_file - Convenience wrapper that reads a file first + +Connects to: + parser/common.v - calls parse_network_addr, parse_port_spec, parse_port_list, + parse_protocol, parse_action, parse_table, parse_chain_type, + parse_conn_states for every token type + models/models.v - imports Rule, Ruleset, MatchCriteria, NetworkAddr, Action, Table + main.v - called from load_ruleset when format is .iptables +*/ module parser diff --git a/PROJECTS/beginner/firewall-rule-engine/src/parser/nftables.v b/PROJECTS/beginner/firewall-rule-engine/src/parser/nftables.v index 4fe52b42..b78eb231 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/parser/nftables.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/parser/nftables.v @@ -1,5 +1,29 @@ -// ©AngelaMos | 2026 -// nftables.v +/* +©AngelaMos | 2026 +nftables.v + +Parser for nftables ruleset format + +Uses recursive descent to walk the nested brace structure: table blocks +contain chain blocks which contain rule lines. parse_nft_table extracts +the table name (skipping address family keywords like inet/ip/ip6), +parse_nft_chain reads the "type filter hook ... policy ..." preamble to +capture chain policies, and parse_nft_rule tokenizes individual rule +lines. Port matching handles both single ports ("dport 22") and set +syntax ("dport { 80, 443 }"). Supports IPv4/IPv6 address matching via +"ip saddr"/"ip6 saddr", conntrack via "ct state", rate limiting, NAT +actions (dnat/snat/masquerade), and log with prefix. + +Key exports: + parse_nftables - Parses a full nftables ruleset string into a Ruleset + parse_nftables_file - Convenience wrapper that reads a file first + +Connects to: + parser/common.v - calls parse_network_addr, parse_port_spec, parse_protocol, + parse_action, parse_table, parse_chain_type, parse_conn_states + models/models.v - imports Rule, Ruleset, MatchCriteria, NetworkAddr, Action, Table + main.v - called from load_ruleset when format is .nftables +*/ module parser diff --git a/PROJECTS/beginner/firewall-rule-engine/src/parser/parser_test.v b/PROJECTS/beginner/firewall-rule-engine/src/parser/parser_test.v index 4e54a5ba..2ad52cca 100644 --- a/PROJECTS/beginner/firewall-rule-engine/src/parser/parser_test.v +++ b/PROJECTS/beginner/firewall-rule-engine/src/parser/parser_test.v @@ -1,5 +1,31 @@ -// ©AngelaMos | 2026 -// parser_test.v +/* +©AngelaMos | 2026 +parser_test.v + +Tests for parsing primitives, format detection, and full ruleset parsing + +Covers every public function in common.v, iptables.v, and nftables.v. +Primitive tests verify network address CIDR and negation parsing, single +and ranged port specs, protocol name and number resolution, action and +table mapping, chain type classification, and conntrack state flag +combinations. Format detection tests confirm heuristic identification of +iptables table headers, chain policies, rule lines, and nftables table +blocks. Integration tests load fixture files from testdata/ to verify +rule counts, policy extraction, SSH port rules, conntrack rules, +multiport parsing, rate limits, NAT/masquerade actions, and IPv6 +address handling. Also tests tokenize_iptables for quoted strings, +ip_to_u32 for valid/invalid addresses, and goto (-g/--goto) handling. + +Connects to: + parser/common.v - tests parse_network_addr, parse_port_spec, parse_port_list, + parse_protocol, parse_action, parse_table, parse_chain_type, + parse_conn_states, detect_format + parser/iptables.v - tests parse_iptables, tokenize_iptables + parser/nftables.v - tests parse_nftables + models/models.v - tests ip_to_u32 directly + testdata/ - loads iptables_basic, iptables_complex, iptables_conflicts, + nftables_basic, nftables_complex, nftables_conflicts fixtures +*/ module parser diff --git a/PROJECTS/beginner/hash-cracker/main.cpp b/PROJECTS/beginner/hash-cracker/main.cpp index 4a380385..08e11112 100644 --- a/PROJECTS/beginner/hash-cracker/main.cpp +++ b/PROJECTS/beginner/hash-cracker/main.cpp @@ -1,5 +1,33 @@ -// ©AngelaMos | 2026 -// main.cpp +/* +©AngelaMos | 2026 +main.cpp + +CLI entry point with hash type dispatch and attack mode selection + +Parses command-line arguments via boost::program_options for hash target, +algorithm (md5/sha1/sha256/sha512/auto), attack mode (dictionary, brute- +force, or rule-based), charset selection, salt, thread count, and JSON +output. build_charset assembles a character set from comma-separated +tokens (lower, upper, digits, special). dispatch_hasher selects the +concrete EVPHasher instantiation at runtime via a switch on HashType, +then dispatch_attack picks the attack strategy (BruteForceAttack, +RuleAttack, or DictionaryAttack) based on config flags. When auto- +detection is requested, HashDetector identifies the algorithm from hex +digest length. + +Key exports: + main - Entry point, returns 0 on crack success, 1 on failure or exhaustion + +Connects to: + config/Config.hpp - CrackConfig, CrackResult, charset constants, defaults + core/Concepts.hpp - CrackError enum for error propagation + core/Engine.hpp - Engine::crack template drives the crack session + hash/HashDetector.hpp - HashDetector::detect for auto-detection + hash/MD5Hasher.hpp et al. - Concrete hasher types for dispatch + attack/BruteForceAttack.hpp - BruteForceAttack for exhaustive mode + attack/DictionaryAttack.hpp - DictionaryAttack for wordlist mode + attack/RuleAttack.hpp - RuleAttack for mutation mode +*/ #include #include diff --git a/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.cpp b/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.cpp index b2c0532b..a04332e4 100644 --- a/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.cpp +++ b/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.cpp @@ -1,5 +1,27 @@ -// ©AngelaMos | 2026 -// BruteForceAttack.cpp +/* +©AngelaMos | 2026 +BruteForceAttack.cpp + +Keyspace generation from charset and max length with parallel partitioning + +compute_keyspace calculates the total number of candidates across all +lengths from 1 to max_length (sum of charset_size^len). The constructor +divides this space evenly among threads using index-based partitioning +with remainder distribution. index_to_candidate converts a flat index +into a string by first determining the target length (walking cumulative +powers) then extracting each character position via modular arithmetic, +similar to converting a number to a variable-base representation. + +Key exports: + BruteForceAttack::BruteForceAttack - Constructor with charset, max_length, thread partitioning + BruteForceAttack::next - Returns next candidate or AttackComplete + BruteForceAttack::total - Total keyspace size + BruteForceAttack::progress - Candidates generated so far by this partition + +Connects to: + attack/BruteForceAttack.hpp - class declaration + core/Concepts.hpp - AttackComplete sentinel +*/ #include "src/attack/BruteForceAttack.hpp" #include diff --git a/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.hpp b/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.hpp index b04aee75..27144374 100644 --- a/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.hpp +++ b/PROJECTS/beginner/hash-cracker/src/attack/BruteForceAttack.hpp @@ -1,5 +1,14 @@ -// ©AngelaMos | 2026 -// BruteForceAttack.hpp +/* +©AngelaMos | 2026 +BruteForceAttack.hpp + +Exhaustive keyspace enumeration with thread-partitioned ranges + +Connects to: + attack/BruteForceAttack.cpp - implementation of next(), index_to_candidate + core/Concepts.hpp - satisfies AttackStrategy concept, uses AttackComplete + core/Engine.hpp - instantiated when cfg.bruteforce is true +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.cpp b/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.cpp index c419590e..a3930c7d 100644 --- a/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.cpp +++ b/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.cpp @@ -1,5 +1,28 @@ -// ©AngelaMos | 2026 -// DictionaryAttack.cpp +/* +©AngelaMos | 2026 +DictionaryAttack.cpp + +Wordlist reading over a memory-mapped file with thread-safe partitioning + +create() opens the wordlist via MappedFile, counts total lines, divides +them evenly among threads (with remainder distribution), then walks +forward through the mapped buffer to find each thread's start and end +byte offsets. next() scans forward from current_offset_ to the next +newline, strips trailing \r for Windows-format wordlists, and returns +the word. Skips blank lines. Returns AttackComplete when the thread's +partition is exhausted. + +Key exports: + DictionaryAttack::create - Factory that opens and partitions a wordlist file + DictionaryAttack::next - Returns next word or AttackComplete + DictionaryAttack::total - Total words in this thread's partition + DictionaryAttack::progress - Words read so far + +Connects to: + attack/DictionaryAttack.hpp - class declaration + io/MappedFile.hpp - MappedFile for zero-copy file access + core/Concepts.hpp - CrackError and AttackComplete types +*/ #include "src/attack/DictionaryAttack.hpp" #include diff --git a/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.hpp b/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.hpp index 20d55341..11d6770a 100644 --- a/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.hpp +++ b/PROJECTS/beginner/hash-cracker/src/attack/DictionaryAttack.hpp @@ -1,5 +1,16 @@ -// ©AngelaMos | 2026 -// DictionaryAttack.hpp +/* +©AngelaMos | 2026 +DictionaryAttack.hpp + +Memory-mapped wordlist attack with line-based thread partitioning + +Connects to: + attack/DictionaryAttack.cpp - implementation of create(), next() + io/MappedFile.hpp - MappedFile for zero-copy file access + core/Concepts.hpp - satisfies AttackStrategy concept + core/Engine.hpp - default attack when no flags set + attack/RuleAttack.hpp - RuleAttack wraps DictionaryAttack internally +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.cpp b/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.cpp index 30110386..e737a969 100644 --- a/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.cpp +++ b/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.cpp @@ -1,5 +1,28 @@ -// ©AngelaMos | 2026 -// RuleAttack.cpp +/* +©AngelaMos | 2026 +RuleAttack.cpp + +Mutation-based attack combining dictionary words with transformation rules + +For each word from the underlying DictionaryAttack, load_next_word() +generates the full mutation set via RuleSet::apply_all (capitalize, +uppercase, leet speak, append/prepend digits 0-999, reverse, toggle +case). When chain_rules is true, it takes the first-pass mutations and +runs them through apply_all again, producing double-mutated candidates. +next() iterates through the mutations_ buffer for the current word, +calling load_next_word() when the buffer is exhausted. + +Key exports: + RuleAttack::create - Factory wrapping a DictionaryAttack with mutation config + RuleAttack::next - Returns next mutation or AttackComplete + RuleAttack::total - Underlying dictionary word count (mutations multiply this) + RuleAttack::progress - Total candidates yielded so far + +Connects to: + attack/RuleAttack.hpp - class declaration + attack/DictionaryAttack.hpp - DictionaryAttack for word iteration + rules/RuleSet.hpp - RuleSet::apply_all for generating mutations +*/ #include "src/attack/RuleAttack.hpp" #include "src/rules/RuleSet.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.hpp b/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.hpp index f9379b0e..34d3bc3c 100644 --- a/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.hpp +++ b/PROJECTS/beginner/hash-cracker/src/attack/RuleAttack.hpp @@ -1,5 +1,19 @@ -// ©AngelaMos | 2026 -// RuleAttack.hpp +/* +©AngelaMos | 2026 +RuleAttack.hpp + +Dictionary attack augmented with password mutation rules + +Wraps a DictionaryAttack and applies RuleSet mutations to each word. +When chain_rules is enabled, mutations of mutations are also generated, +greatly expanding the candidate space. + +Connects to: + attack/RuleAttack.cpp - implementation of create(), next(), load_next_word() + attack/DictionaryAttack.hpp - DictionaryAttack used internally for word iteration + core/Concepts.hpp - satisfies AttackStrategy concept + core/Engine.hpp - instantiated when cfg.use_rules is true +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/config/Config.hpp b/PROJECTS/beginner/hash-cracker/src/config/Config.hpp index 87bd67ef..13c53142 100644 --- a/PROJECTS/beginner/hash-cracker/src/config/Config.hpp +++ b/PROJECTS/beginner/hash-cracker/src/config/Config.hpp @@ -1,5 +1,34 @@ -// ©AngelaMos | 2026 -// Config.hpp +/* +©AngelaMos | 2026 +Config.hpp + +Application-wide constants and configuration structs + +Defines every constant the tool references: character sets for brute- +force, hex digest lengths for hash detection, ANSI color codes for +terminal output, Unicode box-drawing and symbol characters for the +progress display, and numeric defaults (thread count, max brute-force +length, progress update interval). CrackConfig carries all user-facing +options from the CLI into the Engine. CrackResult holds the output of +a successful crack including plaintext, timing, and throughput stats. + +Key exports: + config::VERSION, APP_NAME - Binary identity + config::CHARSET_* - Character sets for brute-force generation + config::MD5_HEX_LENGTH et al. - Expected hex digest lengths per algorithm + config::color::* - ANSI escape sequences + config::box::* - Box-drawing Unicode characters + config::symbol::* - Status symbols (check, cross, arrow, etc.) + CrackConfig - All runtime options for a crack session + CrackResult - Output struct with plaintext and performance metrics + +Connects to: + main.cpp - CrackConfig populated from CLI args, CrackResult written to JSON + core/Engine.hpp - reads CrackConfig, produces CrackResult + hash/HashDetector.cpp - reads hex length constants for detection + display/Progress.cpp - reads color, box, symbol constants for rendering + rules/RuleSet.cpp - reads MAX_APPEND_DIGIT, MAX_PREPEND_DIGIT +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/core/Concepts.hpp b/PROJECTS/beginner/hash-cracker/src/core/Concepts.hpp index 7394dab3..59d1ee90 100644 --- a/PROJECTS/beginner/hash-cracker/src/core/Concepts.hpp +++ b/PROJECTS/beginner/hash-cracker/src/core/Concepts.hpp @@ -1,5 +1,33 @@ -// ©AngelaMos | 2026 -// Concepts.hpp +/* +©AngelaMos | 2026 +Concepts.hpp + +C++20 concepts, error types, and contract definitions for the crack pipeline + +Defines the two core concepts that Engine::crack is templated on: Hasher +(requires hash(string_view)->string, name()->string_view, digest_length() +->size_t) and AttackStrategy (requires next()->expected, total()->size_t, progress()->size_t). CrackError is the +unified error enum propagated via std::expected throughout the tool. +AttackComplete is a sentinel type returned by attack strategies when +their candidate space is exhausted. + +Key exports: + Hasher - Concept constraining hash algorithm implementations + AttackStrategy - Concept constraining candidate generators + CrackError - Error enum (FileNotFound, InvalidHash, Exhausted, etc.) + AttackComplete - Empty sentinel signaling end of candidate stream + crack_error_message - Maps CrackError to human-readable string_view + +Connects to: + core/Engine.hpp - Engine::crack constrained by both concepts + hash/EVPHasher.hpp - EVPHasher satisfies the Hasher concept + attack/BruteForceAttack.hpp - BruteForceAttack satisfies AttackStrategy + attack/DictionaryAttack.hpp - DictionaryAttack satisfies AttackStrategy + attack/RuleAttack.hpp - RuleAttack satisfies AttackStrategy + io/MappedFile.hpp - returns CrackError on failure + main.cpp - uses crack_error_message for error display +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/core/Engine.hpp b/PROJECTS/beginner/hash-cracker/src/core/Engine.hpp index 901d23a0..ba8c48bb 100644 --- a/PROJECTS/beginner/hash-cracker/src/core/Engine.hpp +++ b/PROJECTS/beginner/hash-cracker/src/core/Engine.hpp @@ -1,5 +1,33 @@ -// ©AngelaMos | 2026 -// Engine.hpp +/* +©AngelaMos | 2026 +Engine.hpp + +Template-driven crack engine orchestrating threads, attacks, and progress + +Engine::crack is the single function that runs an entire crack +session. It creates a ThreadPool, spawns one attack instance per thread +(each partitioned to a disjoint slice of the keyspace or wordlist), and +runs a background jthread for progress display updates. Each worker +thread hashes candidates through the Hasher H, prepending or appending +salt if configured, and checks against the target hash. The first match +sets SharedState::found atomically and stores the plaintext. Candidate +counts are flushed from thread-local accumulators to the shared atomic +counter every 1024 iterations to reduce contention. Returns a CrackResult +on success or CrackError::Exhausted when all candidates are spent. + +Key exports: + Engine::crack - Runs a full crack session for Hasher H and AttackStrategy A + +Connects to: + config/Config.hpp - reads CrackConfig options, produces CrackResult + core/Concepts.hpp - Hasher and AttackStrategy concept constraints + threading/ThreadPool.hpp - ThreadPool for parallel worker dispatch + display/Progress.hpp - Progress for live terminal feedback + attack/BruteForceAttack.hpp - instantiated when A = BruteForceAttack + attack/DictionaryAttack.hpp - instantiated when A = DictionaryAttack + attack/RuleAttack.hpp - instantiated when A = RuleAttack + main.cpp - called from dispatch_attack +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp b/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp index c2373464..7202ee69 100644 --- a/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp +++ b/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp @@ -1,5 +1,32 @@ -// ©AngelaMos | 2026 -// Progress.cpp +/* +©AngelaMos | 2026 +Progress.cpp + +ANSI terminal progress bar with speed, ETA, and result formatting + +print_banner draws a Unicode box with algorithm, attack mode, and thread +count. update() runs in a background jthread and uses ANSI escape +sequences (\033[3A) to overwrite the previous three lines with an +updated progress bar, hash speed, elapsed time, ETA, and candidate +count. render_bar builds a filled/empty block character bar scaled to +terminal width (queried via ioctl TIOCGWINSZ). format_count humanizes +large numbers (K/M/B suffixes). print_cracked clears the progress area +and shows the cracked password with green highlight. print_exhausted +shows the red failure state. All output is suppressed when stdout is not +a TTY, falling back to a single-line plaintext result. + +Key exports: + Progress::print_banner - Draw boxed header on session start + Progress::update - Refresh progress bar (called from jthread loop) + Progress::print_cracked - Display success result + Progress::print_exhausted - Display exhaustion result + Progress::is_tty - Check if stdout is a terminal + +Connects to: + display/Progress.hpp - class declaration + config/Config.hpp - color, box, symbol constants, PROGRESS_BAR_MIN_WIDTH + core/Engine.hpp - constructs Progress, spawns update jthread, calls print_* +*/ #include "src/display/Progress.hpp" #include "src/config/Config.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/display/Progress.hpp b/PROJECTS/beginner/hash-cracker/src/display/Progress.hpp index 9084b2a6..c9e7b304 100644 --- a/PROJECTS/beginner/hash-cracker/src/display/Progress.hpp +++ b/PROJECTS/beginner/hash-cracker/src/display/Progress.hpp @@ -1,5 +1,14 @@ -// ©AngelaMos | 2026 -// Progress.hpp +/* +©AngelaMos | 2026 +Progress.hpp + +Terminal progress display with live speed, ETA, and result rendering + +Connects to: + display/Progress.cpp - implementation of banner, update loop, and result display + config/Config.hpp - color, box, symbol constants and PROGRESS_* settings + core/Engine.hpp - Engine::crack creates Progress and calls update in a jthread +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/hash/EVPHasher.hpp b/PROJECTS/beginner/hash-cracker/src/hash/EVPHasher.hpp index 4223d077..e2ebd751 100644 --- a/PROJECTS/beginner/hash-cracker/src/hash/EVPHasher.hpp +++ b/PROJECTS/beginner/hash-cracker/src/hash/EVPHasher.hpp @@ -1,5 +1,34 @@ -// ©AngelaMos | 2026 -// EVPHasher.hpp +/* +©AngelaMos | 2026 +EVPHasher.hpp + +OpenSSL EVP-based hash implementation with tag dispatch and compile-time hex table + +Uses the EVP high-level API so the same code path handles MD5, SHA-1, +SHA-256, and SHA-512 without per-algorithm boilerplate. Each algorithm is +identified by a tag struct (MD5Tag, SHA1Tag, SHA256Tag, SHA512Tag) that +provides the EVP_MD factory pointer, display name, and expected hex +length. EVPHasher satisfies the Hasher concept from Concepts.hpp. +The HEX_TABLE constexpr lookup array converts raw digest bytes to hex +characters in a single indexed load per byte, avoiding the overhead of +std::format or snprintf in the hot path. + +Key exports: + EVPHasher - Template class satisfying the Hasher concept via OpenSSL EVP + MD5Hasher - Type alias for EVPHasher + SHA1Hasher - Type alias for EVPHasher + SHA256Hasher - Type alias for EVPHasher + SHA512Hasher - Type alias for EVPHasher + HEX_TABLE - Compile-time byte-to-hex lookup array + +Connects to: + core/Concepts.hpp - satisfies the Hasher concept + core/Engine.hpp - Engine::crack instantiates EVPHasher per thread + hash/MD5Hasher.hpp - forwarding header that includes this file + hash/SHA1Hasher.hpp - forwarding header + hash/SHA256Hasher.hpp - forwarding header + hash/SHA512Hasher.hpp - forwarding header +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.cpp b/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.cpp index 22d7335b..579f3bcf 100644 --- a/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.cpp +++ b/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.cpp @@ -1,5 +1,23 @@ -// ©AngelaMos | 2026 -// HashDetector.cpp +/* +©AngelaMos | 2026 +HashDetector.cpp + +Hash algorithm detection via hex character validation and length matching + +Validates that every character in the input is hexadecimal, then switches +on the string length to identify the algorithm: 32 chars for MD5, 40 for +SHA-1, 64 for SHA-256, 128 for SHA-512. Returns CrackError::InvalidHash +for non-hex input or unrecognized lengths. + +Key exports: + HashDetector::detect - Returns HashType or CrackError based on hex length + +Connects to: + hash/HashDetector.hpp - class declaration and HashType enum + config/Config.hpp - MD5_HEX_LENGTH, SHA1_HEX_LENGTH, SHA256_HEX_LENGTH, + SHA512_HEX_LENGTH constants + main.cpp - called when --type=auto +*/ #include "src/hash/HashDetector.hpp" #include "src/config/Config.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.hpp b/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.hpp index 0949b63e..4c339fe4 100644 --- a/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.hpp +++ b/PROJECTS/beginner/hash-cracker/src/hash/HashDetector.hpp @@ -1,5 +1,14 @@ -// ©AngelaMos | 2026 -// HashDetector.hpp +/* +©AngelaMos | 2026 +HashDetector.hpp + +Hash algorithm auto-detection by hex digest length + +Connects to: + hash/HashDetector.cpp - implementation of detect() + core/Concepts.hpp - CrackError for invalid/unsupported hash errors + main.cpp - called when --type=auto (the default) +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/hash/MD5Hasher.hpp b/PROJECTS/beginner/hash-cracker/src/hash/MD5Hasher.hpp index 725ece6b..39e7a0a7 100644 --- a/PROJECTS/beginner/hash-cracker/src/hash/MD5Hasher.hpp +++ b/PROJECTS/beginner/hash-cracker/src/hash/MD5Hasher.hpp @@ -1,5 +1,12 @@ -// ©AngelaMos | 2026 -// MD5Hasher.hpp +/* +©AngelaMos | 2026 +MD5Hasher.hpp + +Forwarding header exposing MD5Hasher from EVPHasher.hpp + +Connects to: + hash/EVPHasher.hpp - defines MD5Hasher as EVPHasher +*/ #pragma once #include "src/hash/EVPHasher.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/hash/SHA1Hasher.hpp b/PROJECTS/beginner/hash-cracker/src/hash/SHA1Hasher.hpp index 884a3ee6..46bfac6a 100644 --- a/PROJECTS/beginner/hash-cracker/src/hash/SHA1Hasher.hpp +++ b/PROJECTS/beginner/hash-cracker/src/hash/SHA1Hasher.hpp @@ -1,5 +1,12 @@ -// ©AngelaMos | 2026 -// SHA1Hasher.hpp +/* +©AngelaMos | 2026 +SHA1Hasher.hpp + +Forwarding header exposing SHA1Hasher from EVPHasher.hpp + +Connects to: + hash/EVPHasher.hpp - defines SHA1Hasher as EVPHasher +*/ #pragma once #include "src/hash/EVPHasher.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/hash/SHA256Hasher.hpp b/PROJECTS/beginner/hash-cracker/src/hash/SHA256Hasher.hpp index 3f109540..cea83e3c 100644 --- a/PROJECTS/beginner/hash-cracker/src/hash/SHA256Hasher.hpp +++ b/PROJECTS/beginner/hash-cracker/src/hash/SHA256Hasher.hpp @@ -1,5 +1,12 @@ -// ©AngelaMos | 2026 -// SHA256Hasher.hpp +/* +©AngelaMos | 2026 +SHA256Hasher.hpp + +Forwarding header exposing SHA256Hasher from EVPHasher.hpp + +Connects to: + hash/EVPHasher.hpp - defines SHA256Hasher as EVPHasher +*/ #pragma once #include "src/hash/EVPHasher.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/hash/SHA512Hasher.hpp b/PROJECTS/beginner/hash-cracker/src/hash/SHA512Hasher.hpp index caf29fe2..4a102d0d 100644 --- a/PROJECTS/beginner/hash-cracker/src/hash/SHA512Hasher.hpp +++ b/PROJECTS/beginner/hash-cracker/src/hash/SHA512Hasher.hpp @@ -1,5 +1,12 @@ -// ©AngelaMos | 2026 -// SHA512Hasher.hpp +/* +©AngelaMos | 2026 +SHA512Hasher.hpp + +Forwarding header exposing SHA512Hasher from EVPHasher.hpp + +Connects to: + hash/EVPHasher.hpp - defines SHA512Hasher as EVPHasher +*/ #pragma once #include "src/hash/EVPHasher.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/io/MappedFile.cpp b/PROJECTS/beginner/hash-cracker/src/io/MappedFile.cpp index f8a4b067..e80eca1d 100644 --- a/PROJECTS/beginner/hash-cracker/src/io/MappedFile.cpp +++ b/PROJECTS/beginner/hash-cracker/src/io/MappedFile.cpp @@ -1,5 +1,25 @@ -// ©AngelaMos | 2026 -// MappedFile.cpp +/* +©AngelaMos | 2026 +MappedFile.cpp + +POSIX mmap-based file mapping with RAII cleanup and move semantics + +open() calls POSIX open(2), fstat for size, and mmap with PROT_READ | +MAP_PRIVATE. madvise(MADV_SEQUENTIAL) hints to the kernel that the file +will be read linearly, which improves readahead performance for large +wordlists. The destructor munmaps the region and closes the fd. Move +constructor and assignment transfer ownership by nulling the source +pointers, preventing double-unmap. Copy is deleted. + +Key exports: + MappedFile::open - Maps a file read-only, returns MappedFile or CrackError + MappedFile::data - Pointer to mapped memory + MappedFile::size - File size in bytes + +Connects to: + io/MappedFile.hpp - class declaration + attack/DictionaryAttack.cpp - open() called in create(), data() read in next() +*/ #include "src/io/MappedFile.hpp" #include diff --git a/PROJECTS/beginner/hash-cracker/src/io/MappedFile.hpp b/PROJECTS/beginner/hash-cracker/src/io/MappedFile.hpp index a31e8d35..35b594e9 100644 --- a/PROJECTS/beginner/hash-cracker/src/io/MappedFile.hpp +++ b/PROJECTS/beginner/hash-cracker/src/io/MappedFile.hpp @@ -1,5 +1,14 @@ -// ©AngelaMos | 2026 -// MappedFile.hpp +/* +©AngelaMos | 2026 +MappedFile.hpp + +RAII wrapper for read-only memory-mapped files + +Connects to: + io/MappedFile.cpp - implementation using mmap/munmap + attack/DictionaryAttack.hpp - DictionaryAttack holds a MappedFile + core/Concepts.hpp - CrackError for open failures +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.cpp b/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.cpp index c308b482..9e0d0318 100644 --- a/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.cpp +++ b/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.cpp @@ -1,5 +1,35 @@ -// ©AngelaMos | 2026 -// RuleSet.cpp +/* +©AngelaMos | 2026 +RuleSet.cpp + +Coroutine-based password mutation generators + +Each static method is a C++23 generator that co_yields transformed +candidates from a base word. capitalize_first uppercases the first +character. uppercase_all transforms every character. leet_speak applies +a fixed substitution map (a->@, e->3, i->1, o->0, s->$, t->7). +append_digits and prepend_digits yield the word with every integer from +0 to MAX_APPEND_DIGIT/MAX_PREPEND_DIGIT (999 by default, producing 1000 +candidates each). reverse yields the reversed string. toggle_case swaps +upper to lower and vice versa. apply_all chains all generators together +using co_yield std::ranges::elements_of, producing ~2005 mutations per +input word. + +Key exports: + RuleSet::capitalize_first - Uppercase first letter + RuleSet::uppercase_all - Uppercase all letters + RuleSet::leet_speak - Common character substitutions + RuleSet::append_digits - Word + 0..999 + RuleSet::prepend_digits - 0..999 + word + RuleSet::reverse - Reversed string + RuleSet::toggle_case - Swap upper/lower case + RuleSet::apply_all - Chains all above generators into one stream + +Connects to: + rules/RuleSet.hpp - class declaration + config/Config.hpp - MAX_APPEND_DIGIT, MAX_PREPEND_DIGIT + attack/RuleAttack.cpp - calls apply_all for each word +*/ #include "src/rules/RuleSet.hpp" #include "src/config/Config.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.hpp b/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.hpp index b5b8563e..bf291869 100644 --- a/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.hpp +++ b/PROJECTS/beginner/hash-cracker/src/rules/RuleSet.hpp @@ -1,5 +1,14 @@ -// ©AngelaMos | 2026 -// RuleSet.hpp +/* +©AngelaMos | 2026 +RuleSet.hpp + +Password mutation rules using C++23 std::generator coroutines + +Connects to: + rules/RuleSet.cpp - implementation of all mutation generators + attack/RuleAttack.cpp - calls apply_all() for each dictionary word + config/Config.hpp - MAX_APPEND_DIGIT, MAX_PREPEND_DIGIT limits +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp b/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp index 2f45e9d4..27691aed 100644 --- a/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp +++ b/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp @@ -1,5 +1,20 @@ -// ©AngelaMos | 2026 -// ThreadPool.cpp +/* +©AngelaMos | 2026 +ThreadPool.cpp + +Thread pool and shared state implementation + +set_result uses a relaxed store on the found flag for speed (all readers +also use relaxed loads) and a mutex guard for the result string to +prevent races when multiple threads find the answer simultaneously; only +the first write wins. The constructor resolves thread_count 0 to +hardware_concurrency. run() spawns jthreads in a vector; they auto-join +on destruction when the vector goes out of scope. + +Connects to: + threading/ThreadPool.hpp - class declarations + core/Engine.hpp - Engine::crack calls run() with a lambda +*/ #include "src/threading/ThreadPool.hpp" diff --git a/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.hpp b/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.hpp index 3da56245..0145073d 100644 --- a/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.hpp +++ b/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.hpp @@ -1,5 +1,27 @@ -// ©AngelaMos | 2026 -// ThreadPool.hpp +/* +©AngelaMos | 2026 +ThreadPool.hpp + +Lightweight thread pool with shared atomic state for crack coordination + +SharedState holds the found flag (atomic), tested_count (atomic +size_t), and the cracked plaintext behind a mutex. The cache-line +aligned atomics (alignas(64)) prevent false sharing between the hot +found flag and the counter. ThreadPool spawns jthreads that each call +the user's WorkFn with their thread ID, total thread count, and a +reference to SharedState. + +Key exports: + SharedState - Shared atomic found flag, tested count, and result + SharedState::set_result - Thread-safe first-writer-wins plaintext storage + ThreadPool - Spawns jthreads and joins on destruction + ThreadPool::run - Launches work across all threads + ThreadPool::state - Access to SharedState for result checking + +Connects to: + threading/ThreadPool.cpp - implementation of set_result, constructor, run + core/Engine.hpp - Engine::crack creates and runs the pool +*/ #pragma once diff --git a/PROJECTS/beginner/hash-cracker/tests/test_bruteforce_attack.cpp b/PROJECTS/beginner/hash-cracker/tests/test_bruteforce_attack.cpp index 8bf83562..bcb8b413 100644 --- a/PROJECTS/beginner/hash-cracker/tests/test_bruteforce_attack.cpp +++ b/PROJECTS/beginner/hash-cracker/tests/test_bruteforce_attack.cpp @@ -1,5 +1,17 @@ -// ©AngelaMos | 2026 -// test_bruteforce_attack.cpp +/* +©AngelaMos | 2026 +test_bruteforce_attack.cpp + +Tests for brute-force keyspace generation and thread partitioning + +Verifies single-char generation, multi-length enumeration up to +max_length, correct keyspace total (sum of charset^len), and that +splitting across two threads produces the same combined set without +duplicates or gaps. + +Connects to: + attack/BruteForceAttack.hpp - BruteForceAttack tested +*/ #include #include "src/attack/BruteForceAttack.hpp" diff --git a/PROJECTS/beginner/hash-cracker/tests/test_dictionary_attack.cpp b/PROJECTS/beginner/hash-cracker/tests/test_dictionary_attack.cpp index 64ef8c9a..c98bc12b 100644 --- a/PROJECTS/beginner/hash-cracker/tests/test_dictionary_attack.cpp +++ b/PROJECTS/beginner/hash-cracker/tests/test_dictionary_attack.cpp @@ -1,5 +1,18 @@ -// ©AngelaMos | 2026 -// test_dictionary_attack.cpp +/* +©AngelaMos | 2026 +test_dictionary_attack.cpp + +Tests for memory-mapped wordlist reading and thread partitioning + +Loads tests/data/small_wordlist.txt (10 words) and verifies all words +are read in order, first/last word content, correct total count, +two-thread partitioning that covers all words without overlap, and +graceful CrackError on missing files. + +Connects to: + attack/DictionaryAttack.hpp - DictionaryAttack tested + tests/data/small_wordlist.txt - fixture wordlist +*/ #include #include "src/attack/DictionaryAttack.hpp" diff --git a/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp b/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp index 5a6972ab..4fe08ffb 100644 --- a/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp +++ b/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp @@ -1,5 +1,20 @@ -// ©AngelaMos | 2026 -// test_engine.cpp +/* +©AngelaMos | 2026 +test_engine.cpp + +End-to-end tests for the crack engine + +Verifies Engine::crack with SHA256Hasher + DictionaryAttack finds +"password" from the test wordlist. Confirms CrackError::Exhausted when +the target hash is not in the wordlist. Tests salt support by cracking +a prepend-salted hash. + +Connects to: + core/Engine.hpp - Engine::crack tested + hash/SHA256Hasher.hpp - SHA256Hasher used in all tests + attack/DictionaryAttack.hpp - DictionaryAttack as the attack strategy + tests/data/small_wordlist.txt - fixture wordlist +*/ #include #include "src/core/Engine.hpp" diff --git a/PROJECTS/beginner/hash-cracker/tests/test_hash_detector.cpp b/PROJECTS/beginner/hash-cracker/tests/test_hash_detector.cpp index 7b32891c..229d2e50 100644 --- a/PROJECTS/beginner/hash-cracker/tests/test_hash_detector.cpp +++ b/PROJECTS/beginner/hash-cracker/tests/test_hash_detector.cpp @@ -1,5 +1,17 @@ -// ©AngelaMos | 2026 -// test_hash_detector.cpp +/* +©AngelaMos | 2026 +test_hash_detector.cpp + +Tests for hash algorithm auto-detection by digest length + +Verifies detection of MD5 (32 chars), SHA-1 (40), SHA-256 (64), and +SHA-512 (128) from real and synthetic hex strings. Confirms rejection +of invalid lengths and non-hex characters with CrackError::InvalidHash. + +Connects to: + hash/HashDetector.hpp - HashDetector::detect tested + core/Concepts.hpp - CrackError enum for error assertions +*/ #include #include "src/hash/HashDetector.hpp" diff --git a/PROJECTS/beginner/hash-cracker/tests/test_hashers.cpp b/PROJECTS/beginner/hash-cracker/tests/test_hashers.cpp index 5b735004..c9f75e2b 100644 --- a/PROJECTS/beginner/hash-cracker/tests/test_hashers.cpp +++ b/PROJECTS/beginner/hash-cracker/tests/test_hashers.cpp @@ -1,5 +1,20 @@ -// ©AngelaMos | 2026 -// test_hashers.cpp +/* +©AngelaMos | 2026 +test_hashers.cpp + +Tests for all four hash algorithm implementations against known vectors + +Verifies MD5, SHA-1, SHA-256, and SHA-512 against NIST/RFC test vectors +for empty string and "password". Checks static name() and digest_length() +properties. Confirms deterministic output and non-empty results. + +Connects to: + hash/MD5Hasher.hpp - MD5Hasher tested + hash/SHA1Hasher.hpp - SHA1Hasher tested + hash/SHA256Hasher.hpp - SHA256Hasher tested + hash/SHA512Hasher.hpp - SHA512Hasher tested + hash/EVPHasher.hpp - underlying implementation +*/ #include #include "src/hash/MD5Hasher.hpp" diff --git a/PROJECTS/beginner/hash-cracker/tests/test_rules.cpp b/PROJECTS/beginner/hash-cracker/tests/test_rules.cpp index 017f4ff3..817d9768 100644 --- a/PROJECTS/beginner/hash-cracker/tests/test_rules.cpp +++ b/PROJECTS/beginner/hash-cracker/tests/test_rules.cpp @@ -1,5 +1,21 @@ -// ©AngelaMos | 2026 -// test_rules.cpp +/* +©AngelaMos | 2026 +test_rules.cpp + +Tests for password mutation rules and rule-based attack integration + +Verifies each individual mutation generator: capitalize_first, uppercase_all, +leet_speak, append_digits (1000 candidates), prepend_digits, reverse, +toggle_case. Tests apply_all produces >2000 mutations and contains expected +entries. Integration tests confirm RuleAttack applies mutations to every +dictionary word and that chain_rules mode produces more candidates than +single-pass mode. + +Connects to: + rules/RuleSet.hpp - individual mutation functions tested + attack/RuleAttack.hpp - RuleAttack integration tested + tests/data/small_wordlist.txt - fixture wordlist +*/ #include #include "src/attack/RuleAttack.hpp" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/01_initial_setup.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/01_initial_setup.sh index e41879a7..2bab9514 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/01_initial_setup.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/01_initial_setup.sh @@ -1,6 +1,24 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # 01_initial_setup.sh +# +# CIS Section 1 checks: Initial Setup +# +# Implements controls 1.1.1-1.5.3 covering filesystem hardening and +# kernel protections. Checks kernel module disabling for eight legacy +# filesystems (cramfs, freevxfs, jffs2, hfs, hfsplus, squashfs, udf, +# vfat) via modprobe.d configs and lsmod. Verifies /tmp as a separate +# partition with noexec, nosuid, and nodev mount options through fstab +# and findmnt. Validates package repository configuration, GPG key +# presence, GRUB bootloader password, grub.cfg permissions and +# ownership, single-user mode authentication (sulogin), ASLR +# (kernel.randomize_va_space=2), core dump restrictions (limits.conf +# + fs.suid_dumpable), and prelink removal. +# +# Connects to: +# lib/registry.sh - record_result for each control +# lib/utils.sh - get_sysctl, read_file, run_cmd, file_exists +# controls/registry_data.sh - defines CIS IDs 1.x.x _check_module_disabled() { local id="$1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/02_services.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/02_services.sh index cc8ef7f5..9c0fb4c0 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/02_services.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/02_services.sh @@ -1,6 +1,24 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # 02_services.sh +# +# CIS Section 2 checks: Services +# +# Implements controls 2.1.1-2.2.16 verifying that unnecessary network +# services are not installed on the system. Checks legacy super-servers +# (xinetd, openbsd-inetd), graphical environment (X Window System), +# and fourteen network daemons: Avahi, CUPS, ISC DHCP, OpenLDAP, NFS, +# BIND DNS, vsftpd, Apache/nginx HTTP, Dovecot IMAP/POP3, Samba, +# Squid proxy, SNMP, and NIS. Each check uses package_is_installed +# with file_exists fallback for binary/config detection. The MTA +# check (2.2.15) inspects Postfix inet_interfaces, Exim +# dc_local_interfaces, and ss port 25 listeners to verify local-only +# mail delivery. Also checks rsync daemon installation. +# +# Connects to: +# lib/registry.sh - record_result for each control +# lib/utils.sh - package_is_installed, file_exists, read_file, +# service_is_enabled, run_cmd check_2_1_1() { local id="2.1.1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/03_network.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/03_network.sh index c4e86a98..89743b16 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/03_network.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/03_network.sh @@ -1,6 +1,24 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # 03_network.sh +# +# CIS Section 3 checks: Network Configuration +# +# Implements controls 3.1.1-3.4.5 auditing kernel network parameters, +# host firewall policy, and uncommon protocol modules. Sysctl checks +# cover IP forwarding, ICMP send_redirects, source routing acceptance, +# ICMP redirect acceptance, martian packet logging, broadcast ICMP +# ignore, bogus ICMP response ignore, reverse path filtering, TCP +# SYN cookies, and IPv6 router advertisement acceptance. Firewall +# checks verify iptables installation, default deny policies on +# INPUT/FORWARD/OUTPUT chains (from live iptables or rules.v4 file), +# and open-port-to-rule coverage via ss. Validates wireless interface +# disabling through rfkill and checks modprobe.d for four uncommon +# protocol modules: DCCP, SCTP, RDS, and TIPC. +# +# Connects to: +# lib/registry.sh - record_result for each control +# lib/utils.sh - get_sysctl, package_is_installed, file_exists, run_cmd check_3_1_1() { local id="3.1.1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/04_logging.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/04_logging.sh index 9409fbc7..074fe665 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/04_logging.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/04_logging.sh @@ -1,6 +1,25 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # 04_logging.sh +# +# CIS Section 4 checks: Logging and Auditing +# +# Implements controls 4.1.1-4.2.4 for the audit subsystem and syslog +# configuration. Verifies auditd is installed, enabled, and configured +# for boot-time auditing (audit=1 in GRUB_CMDLINE_LINUX) with a +# sufficient backlog limit (>=8192). Validates ten categories of audit +# rules by searching /etc/audit/rules.d/ and audit.rules: time changes +# (adjtimex, settimeofday, clock_settime), user/group modifications, +# network environment changes (sethostname, /etc/issue, /etc/hosts), +# MAC policy changes (SELinux/AppArmor paths), login/logout events, +# session initiation (utmp/wtmp/btmp), DAC permission changes (chmod, +# chown family), unauthorized access attempts (EACCES/EPERM), file +# system mounts, and file deletions (unlink/rename). Checks rsyslog +# installation, service status, FileCreateMode, and logging rules. +# +# Connects to: +# lib/registry.sh - record_result for each control +# lib/utils.sh - package_is_installed, file_exists, service_is_enabled check_4_1_1() { local id="4.1.1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access.sh index a5c94241..a9828d0e 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access.sh @@ -1,6 +1,26 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # 05_access.sh +# +# CIS Section 5 checks: Cron and SSH hardening +# +# Implements controls 5.1.1-5.2.14 covering cron daemon and SSH +# server configuration. Verifies cron is enabled and checks +# permissions/ownership on /etc/crontab, cron.hourly, and cron.daily +# (root:root, 0600/0700). SSH checks parse sshd_config for: file +# permissions (0600 root-owned), access restrictions (AllowUsers/ +# AllowGroups/DenyUsers/DenyGroups), host private key permissions, +# LogLevel (INFO or VERBOSE), X11Forwarding (no), MaxAuthTries (<=4), +# IgnoreRhosts (yes), PermitRootLogin (no), PermitEmptyPasswords (no), +# PermitUserEnvironment (no), weak cipher/MAC/KexAlgorithm rejection +# (CBC ciphers, MD5/SHA1-96 MACs, DH group1/14-sha1 KEX), and +# LoginGraceTime (<=60s). Uses _check_ssh_value and _check_ssh_max_int +# helpers for consistent sshd_config parsing. +# +# Connects to: +# lib/registry.sh - record_result for each control +# lib/utils.sh - service_is_enabled, file_exists, run_cmd, +# get_config_value check_5_1_1() { local id="5.1.1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access_password.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access_password.sh index 0577e281..d6ed503a 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access_password.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/05_access_password.sh @@ -1,6 +1,21 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # 05_access_password.sh +# +# CIS Section 5.3-5.5 checks: Password policy and account lockout +# +# Implements controls 5.3.1, 5.4.1-5.4.3, and 5.5.1 for password +# strength and account security. Checks PAM password quality by +# verifying pam_pwquality or pam_cracklib is configured in +# /etc/pam.d/common-password. Validates /etc/login.defs parameters: +# PASS_MAX_DAYS (<=365), PASS_MIN_DAYS (>=1), and PASS_WARN_AGE (>=7) +# using the _check_login_defs_value helper with configurable comparison +# direction. Verifies account lockout is configured via pam_faillock +# or pam_tally2 in /etc/pam.d/common-auth. +# +# Connects to: +# lib/registry.sh - record_result for each control +# lib/utils.sh - file_exists (SYSROOT-aware path resolution) check_5_3_1() { local id="5.3.1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/06_maintenance.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/06_maintenance.sh index 8d28b3e6..c3acc0f6 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/06_maintenance.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/checks/06_maintenance.sh @@ -1,6 +1,22 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # 06_maintenance.sh +# +# CIS Section 6 checks: System Maintenance +# +# Implements controls 6.1.1-6.2.5 for critical file integrity and +# account hygiene. Verifies permissions and ownership on five sensitive +# files (/etc/passwd 644 root:root, /etc/shadow 640 root:shadow, +# /etc/group 644 root:root, /etc/gshadow 640 root:shadow, /etc/passwd- +# 600 root:root) via the _check_file_permissions helper using octal +# comparison. Detects duplicate UIDs, duplicate GIDs, and duplicate +# usernames in /etc/passwd and /etc/group using awk/sort/uniq. Enforces +# that only root has UID 0 and detects legacy NIS '+' entries in +# passwd, shadow, and group files. +# +# Connects to: +# lib/registry.sh - record_result for each control +# lib/utils.sh - run_cmd (stat wrapper) _check_file_permissions() { local id="$1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/cisaudit.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/cisaudit.sh index 93530350..e6796c3a 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/cisaudit.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/cisaudit.sh @@ -1,6 +1,28 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # cisaudit.sh +# +# Main entry point for the CIS benchmark compliance auditor +# +# Parses CLI arguments (level filtering, output format, categories, +# baseline compare/save, test-root override, threshold), sources all +# library modules and check files, then dispatches through run_checks +# to execute registered controls, compute_scores for scoring, and +# generate_report for terminal/JSON/HTML output. Supports --list-controls +# for a dry-run control inventory. Exits non-zero when the overall score +# falls below --threshold. +# +# Connects to: +# lib/constants.sh - version, exit codes, ANSI colors, section names +# lib/utils.sh - logging, progress, environment detection helpers +# lib/registry.sh - control registration and result tracking +# lib/engine.sh - score computation (overall, per-section, per-level) +# lib/report_terminal.sh - ANSI terminal report renderer +# lib/report_json.sh - JSON report emitter +# lib/report_html.sh - standalone HTML report generator +# lib/baseline.sh - baseline save/load/diff +# controls/registry_data.sh - CIS control definitions (70+ register_control calls) +# checks/*.sh - per-section check implementations set -euo pipefail diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/controls/registry_data.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/controls/registry_data.sh index 70eb0d0e..269c8dbe 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/controls/registry_data.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/controls/registry_data.sh @@ -1,6 +1,25 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # registry_data.sh +# +# CIS benchmark control definitions database +# +# Contains 70+ register_control calls that populate the control +# registry with every audited CIS benchmark item. Each entry +# specifies the control ID, parent section name, human-readable +# title, CIS level (1 or 2), scored flag, a rationale paragraph +# explaining the security impact, and a remediation command. Covers +# all six sections: Initial Setup (filesystem modules, /tmp hardening, +# package repos, bootloader, kernel protections), Services (xinetd +# through rsync), Network Configuration (sysctl parameters, firewall +# policies, wireless, uncommon protocols), Logging and Auditing +# (auditd, audit rules, rsyslog), Access/Authentication/Authorization +# (cron, SSH hardening, password policy, account lockout), and System +# Maintenance (file permissions, duplicate IDs, legacy entries). +# +# Connects to: +# lib/registry.sh - register_control function stores each definition +# checks/*.sh - check functions that implement each control ID register_control "1.1.1" \ "Initial Setup" \ diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/baseline.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/baseline.sh index 7b66193a..7799c0e7 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/baseline.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/baseline.sh @@ -1,6 +1,22 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # baseline.sh +# +# Baseline persistence for audit snapshot comparison +# +# Enables saving current audit results as a JSON baseline via +# save_baseline (delegates to emit_json_report), reloading a prior +# baseline with load_baseline (regex-parses control IDs and statuses +# from the JSON without a JSON library), and diffing current results +# against the baseline via diff_baseline which categorizes each +# control as improved, regressed, unchanged, new, or removed and +# prints a colored summary with regression warnings. +# +# Connects to: +# lib/report_json.sh - emit_json_report (called by save_baseline) +# lib/registry.sh - RESULT_ORDER, RESULT_STATUS, CTRL_TITLE +# lib/utils.sh - warn logging function +# lib/constants.sh - ANSI colors, STATUS_PASS/FAIL declare -gA BASELINE_STATUS diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/constants.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/constants.sh index 246411f1..7231f1a2 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/constants.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/constants.sh @@ -1,6 +1,19 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # constants.sh +# +# Global constants and configuration values +# +# Defines the tool version string, CIS benchmark identifier, minimum +# bash version requirement, process exit codes, ANSI terminal color +# escape sequences, audit status labels (PASS/FAIL/WARN/SKIP), the +# default SYSROOT path for live vs test-mode filesystem access, and +# the six CIS section names with their display ordering. +# +# Connects to: +# cisaudit.sh - sourced first, values used by all modules +# lib/utils.sh - ANSI colors used in logging functions +# lib/engine.sh - SECTION_ORDER drives per-section score iteration declare -gr VERSION="1.0.0" declare -gr CIS_BENCHMARK="CIS Debian Linux 12 Benchmark v1.1.0" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/engine.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/engine.sh index e2d513a7..48c4faa7 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/engine.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/engine.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # engine.sh +# +# Score computation engine for CIS audit results +# +# Iterates recorded check results to tally per-section pass/fail/warn/skip +# counts, then computes percentage scores at three levels: per-section +# (SCORE_BY_SECTION), per-CIS-level (SCORE_LEVEL1, SCORE_LEVEL2), and +# overall (SCORE_OVERALL). Scores are calculated as pass/(pass+fail)*100, +# ignoring warn and skip results. Sections with zero scored results +# receive "N/A" instead of a numeric score. +# +# Connects to: +# lib/constants.sh - SECTION_ORDER, STATUS_PASS/FAIL/WARN/SKIP +# lib/registry.sh - RESULT_ORDER, RESULT_STATUS, CTRL_SECTION, +# CTRL_LEVEL, CTRL_SCORED, TOTAL_PASS/FAIL declare -gA SCORE_BY_SECTION declare -gA SECTION_PASS diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/registry.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/registry.sh index 333ed148..d24794d4 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/registry.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/registry.sh @@ -1,6 +1,23 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # registry.sh +# +# Control registration and result tracking system +# +# Provides the in-memory database of CIS controls and their audit +# outcomes using bash associative arrays. register_control stores +# each control's metadata (title, section, level, scored, description, +# remediation) and derives the check function name (check_X_Y_Z). +# record_result captures per-control pass/fail/warn/skip status with +# evidence and maintains running totals. Query helpers retrieve +# controls by section or level. reset_results clears all state for +# test isolation. +# +# Connects to: +# lib/constants.sh - STATUS_PASS/FAIL/WARN/SKIP +# controls/registry_data.sh - populates registry via register_control +# checks/*.sh - check functions call record_result +# lib/engine.sh - reads RESULT_ORDER, RESULT_STATUS, CTRL_* declare -gA CTRL_TITLE declare -gA CTRL_SECTION diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_html.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_html.sh index 36f399ca..86604411 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_html.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_html.sh @@ -1,6 +1,24 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # report_html.sh +# +# Standalone HTML report generator with Tokyo Night theme +# +# Produces a single-file HTML page via emit_html_report containing +# embedded CSS (dark Tokyo Night palette with print-friendly overrides), +# a responsive dashboard with score cards and level badges, a section +# breakdown table with colored progress bars, and collapsible +#
controls for each audited item showing status badges, +# control IDs, evidence blocks, and remediation hints. Failed +# controls auto-expand for immediate visibility. Responsive at +# 640px breakpoint with print media query support. +# +# Connects to: +# lib/constants.sh - VERSION, CIS_BENCHMARK, STATUS_* +# lib/registry.sh - RESULT_ORDER, RESULT_STATUS, RESULT_EVIDENCE, +# CTRL_TITLE, CTRL_SECTION, CTRL_LEVEL +# lib/engine.sh - SCORE_OVERALL, SCORE_LEVEL1, SCORE_LEVEL2, +# SCORE_BY_SECTION, SECTION_PASS/FAIL/WARN/SKIP html_escape() { local s="$1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_json.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_json.sh index b533e547..5b55252d 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_json.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_json.sh @@ -1,6 +1,25 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # report_json.sh +# +# JSON report emitter for machine-readable audit output +# +# Serializes complete audit results into a structured JSON document +# via emit_json_report. Output includes version, CIS benchmark ID, +# ISO 8601 timestamp, hostname, OS detection, a summary block with +# totals and overall/level scores, a sections array with per-section +# pass/fail/warn/skip counts and scores, and a controls array with +# each control's ID, section, title, level, scored flag, status, +# evidence, and remediation. Provides json_escape for safe string +# encoding and null-coalescing for N/A scores. +# +# Connects to: +# lib/constants.sh - VERSION, CIS_BENCHMARK, STATUS_* +# lib/registry.sh - RESULT_ORDER, RESULT_STATUS, RESULT_EVIDENCE, +# CTRL_TITLE, CTRL_SECTION, CTRL_LEVEL, CTRL_SCORED +# lib/engine.sh - SCORE_OVERALL, SCORE_LEVEL1, SCORE_LEVEL2, +# SCORE_BY_SECTION, SECTION_PASS/FAIL/WARN/SKIP +# lib/baseline.sh - save_baseline calls emit_json_report json_escape() { local s="$1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_terminal.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_terminal.sh index a4a907ed..d0652c3a 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_terminal.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/report_terminal.sh @@ -1,6 +1,24 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # report_terminal.sh +# +# ANSI terminal report renderer +# +# Builds a rich terminal report through emit_terminal_report by +# composing four sections: an ASCII art banner with version/hostname/ +# timestamp, a bordered summary card showing overall score and +# pass/fail/warn/skip totals, a section breakdown table with inline +# progress bars and color-coded percentages, and detailed per-control +# results grouped by section with status symbols, evidence lines for +# failures, and remediation hints. All color output uses the ANSI +# escape sequences from constants.sh. +# +# Connects to: +# lib/constants.sh - ANSI colors, VERSION, CIS_BENCHMARK, STATUS_* +# lib/registry.sh - RESULT_ORDER, RESULT_STATUS, RESULT_EVIDENCE, +# CTRL_TITLE, CTRL_SECTION, CTRL_REMEDIATION +# lib/engine.sh - SCORE_OVERALL, SCORE_LEVEL1, SCORE_LEVEL2, +# SCORE_BY_SECTION, SECTION_PASS/FAIL/WARN/SKIP _status_color() { local status="$1" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/utils.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/utils.sh index c2f88258..d2271e6e 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/utils.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/src/lib/utils.sh @@ -1,6 +1,22 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # utils.sh +# +# Shared utility functions for logging and system inspection +# +# Provides ANSI-colored log helpers (info, success, warn, fail) that +# respect the QUIET flag, inline progress display with carriage-return +# clearing, bash version enforcement, root privilege detection, and +# OS identification from /etc/os-release. Wraps all filesystem access +# through SYSROOT so checks work against both the live system and +# test fixture directories: file_exists, read_file, get_sysctl (proc +# tree then sysctl fallback), get_config_value (grep key from config), +# run_cmd (blocks in test mode), and dpkg/systemctl wrappers. +# +# Connects to: +# lib/constants.sh - ANSI color codes, EXIT_FAIL, MIN_BASH_VERSION +# checks/*.sh - check functions call file_exists, get_sysctl, +# package_is_installed, service_is_enabled, etc. info() { [[ "$QUIET" == "true" ]] || echo -e "${CYAN}[*]${RESET} $1" >&2; } success() { [[ "$QUIET" == "true" ]] || echo -e "${GREEN}[✔]${RESET} $1" >&2; } diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_01_initial_setup.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_01_initial_setup.sh index 1703ce86..f5c002d9 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_01_initial_setup.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_01_initial_setup.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_01_initial_setup.sh +# +# Tests for CIS Section 1 (Initial Setup) checks +# +# Uses pass and fail fixture directories to verify cramfs module +# detection (1.1.1), /tmp separate partition (1.2.1), /tmp mount +# options noexec/nosuid/nodev (1.2.2-1.2.4), ASLR enabled status +# (1.5.1), and core dump restrictions (1.5.2). Each test calls the +# corresponding check function against a fixture SYSROOT and asserts +# both the expected status and key evidence substrings. +# +# Connects to: +# checks/01_initial_setup.sh - check functions under test +# tests/test_helpers.sh - setup_test, assert_status, +# assert_evidence_contains test_1_1_1_pass() { CURRENT_TEST="test_1_1_1_pass" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_02_services.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_02_services.sh index cc0ee97b..f81a083e 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_02_services.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_02_services.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_02_services.sh +# +# Tests for CIS Section 2 (Services) checks +# +# Uses pass and fail fixture directories to verify xinetd removal +# (2.1.1), X Window System absence (2.2.1), HTTP server detection +# for apache2 (2.2.9), and MTA local-only configuration via Postfix +# inet_interfaces (2.2.15). Each test asserts the expected status +# and evidence substrings against both compliant and non-compliant +# fixture filesystems. +# +# Connects to: +# checks/02_services.sh - check functions under test +# tests/test_helpers.sh - setup_test, assert_status, +# assert_evidence_contains test_2_1_1_pass() { CURRENT_TEST="test_2_1_1_pass" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_03_network.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_03_network.sh index f021d2bf..b4f054cf 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_03_network.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_03_network.sh @@ -1,6 +1,21 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_03_network.sh +# +# Tests for CIS Section 3 (Network Configuration) checks +# +# Uses pass and fail fixture directories to verify IP forwarding +# disabled (3.1.1), send_redirects disabled (3.1.2), martian packet +# logging enabled (3.2.1), TCP SYN cookies enabled (3.2.5), IPv6 +# router advertisement rejection (3.2.6), and DCCP protocol module +# disabling (3.4.2). Each test asserts the expected status and key +# evidence substrings from sysctl proc tree values and modprobe.d +# configs in the fixture filesystems. +# +# Connects to: +# checks/03_network.sh - check functions under test +# tests/test_helpers.sh - setup_test, assert_status, +# assert_evidence_contains test_3_1_1_pass() { CURRENT_TEST="test_3_1_1_pass" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_04_logging.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_04_logging.sh index ff6fc57a..3c968a44 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_04_logging.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_04_logging.sh @@ -1,6 +1,21 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_04_logging.sh +# +# Tests for CIS Section 4 (Logging and Auditing) checks +# +# Uses pass and fail fixture directories to verify auditd installation +# (4.1.1), boot-time audit parameter in GRUB (4.1.3), audit backlog +# limit (4.1.4), time change audit rules (4.1.5), rsyslog installation +# (4.2.1), rsyslog FileCreateMode (4.2.3), and logging rule presence +# (4.2.4). Each test asserts the expected status and evidence substring +# matches against fixture /etc/default/grub, /etc/audit/rules.d/, and +# /etc/rsyslog.conf contents. +# +# Connects to: +# checks/04_logging.sh - check functions under test +# tests/test_helpers.sh - setup_test, assert_status, +# assert_evidence_contains test_4_1_1_pass() { CURRENT_TEST="test_4_1_1_pass" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access.sh index 344d782b..0fd77534 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access.sh @@ -1,6 +1,21 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_05_access.sh +# +# Tests for CIS Section 5 (Access) SSH hardening checks +# +# Uses pass and fail fixture directories to verify sshd_config +# parsing for LogLevel (5.2.4), MaxAuthTries (5.2.6), X11Forwarding +# (5.2.5), IgnoreRhosts (5.2.7), PermitRootLogin (5.2.8), +# PermitEmptyPasswords (5.2.9), and weak cipher rejection (5.2.11). +# Each test asserts the expected status and evidence substrings +# against fixture sshd_config files containing compliant and +# non-compliant directive values. +# +# Connects to: +# checks/05_access.sh - check functions under test +# tests/test_helpers.sh - setup_test, assert_status, +# assert_evidence_contains test_5_2_4_pass() { CURRENT_TEST="test_5_2_4_pass" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access_password.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access_password.sh index 7503a538..a1c5d586 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access_password.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_05_access_password.sh @@ -1,6 +1,21 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_05_access_password.sh +# +# Tests for CIS Section 5.3-5.5 password policy and account lockout checks +# +# Uses pass and fail fixture directories to verify PAM password quality +# module presence (5.3.1 pam_pwquality), login.defs PASS_MAX_DAYS +# (5.4.1 <=365), PASS_MIN_DAYS (5.4.2 >=1), PASS_WARN_AGE (5.4.3 +# >=7), and account lockout via pam_faillock (5.5.1). Each test +# asserts the expected status and evidence substrings against fixture +# /etc/pam.d/common-password, /etc/login.defs, and /etc/pam.d/ +# common-auth contents. +# +# Connects to: +# checks/05_access_password.sh - check functions under test +# tests/test_helpers.sh - setup_test, assert_status, +# assert_evidence_contains test_5_3_1_pass() { CURRENT_TEST="test_5_3_1_pass" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_06_maintenance.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_06_maintenance.sh index 287815eb..5e968499 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_06_maintenance.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_06_maintenance.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_06_maintenance.sh +# +# Tests for CIS Section 6 (System Maintenance) checks +# +# Uses pass and fail fixture directories to verify duplicate UID +# detection (6.2.1), duplicate GID detection (6.2.2), duplicate +# username detection (6.2.3), UID 0 root-only enforcement (6.2.4), +# and legacy NIS '+' entry detection (6.2.5). Each test asserts +# the expected status and evidence substrings against fixture +# /etc/passwd and /etc/group contents. +# +# Connects to: +# checks/06_maintenance.sh - check functions under test +# tests/test_helpers.sh - setup_test, assert_status, +# assert_evidence_contains test_6_2_1_pass() { CURRENT_TEST="test_6_2_1_pass" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_baseline.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_baseline.sh index d9560aed..261d25af 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_baseline.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_baseline.sh @@ -1,6 +1,22 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_baseline.sh +# +# Tests for baseline save, load, and diff operations +# +# Verifies the full baseline lifecycle: save_baseline produces a JSON +# file that load_baseline can parse back into BASELINE_STATUS with +# correct per-control statuses. Tests diff_baseline with three +# scenarios: all controls unchanged (0 regressed), one regression +# introduced by switching SYSROOT mid-run, and a missing baseline +# file that produces a warning. Also validates that saved baseline +# files are well-formed JSON via assert_json_valid. +# +# Connects to: +# lib/baseline.sh - save_baseline, load_baseline, diff_baseline, +# BASELINE_STATUS +# lib/engine.sh - compute_scores (required before save) +# tests/test_helpers.sh - setup_test, assert_json_valid test_baseline_save_and_load() { CURRENT_TEST="test_baseline_save_and_load" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_engine.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_engine.sh index 2be416de..dcd598a5 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_engine.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_engine.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_engine.sh +# +# Tests for the scoring engine +# +# Verifies compute_scores produces correct results across five +# scenarios: overall score is non-zero when checks produce mixed +# results from fixtures, per-section scores are populated for Initial +# Setup and Network Configuration, all-pass checks yield exactly +# 100.0 overall, one pass plus one fail yields exactly 50.0, and +# reset_results clears all counters and result arrays to zero. +# +# Connects to: +# lib/engine.sh - compute_scores, SCORE_OVERALL, SCORE_BY_SECTION +# lib/registry.sh - reset_results, TOTAL_PASS/FAIL, RESULT_ORDER +# tests/test_helpers.sh - setup_test, TEST_TOTAL/PASS/FAIL counters test_engine_compute_scores_pass_fixtures() { CURRENT_TEST="test_engine_compute_scores_pass_fixtures" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_helpers.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_helpers.sh index a5fb76fd..59b7c40f 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_helpers.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_helpers.sh @@ -1,6 +1,23 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_helpers.sh +# +# Test assertion framework for the bash test harness +# +# Provides the core testing primitives: setup_test resets registry state +# and sets SYSROOT to a fixture directory for isolated filesystem +# checks. assert_status compares a control's recorded status against an +# expected value. assert_evidence_contains checks for a substring in the +# evidence string. assert_json_valid validates JSON via python3's +# json.tool (accepts both file paths and inline strings). print_results +# outputs the total/pass/fail summary and returns non-zero on failures. +# Tracks counts in TEST_PASS, TEST_FAIL, TEST_TOTAL globals. +# +# Connects to: +# lib/registry.sh - reset_results, RESULT_STATUS, RESULT_EVIDENCE +# tests/test_runner.sh - sources this file before running tests +# testdata/fixtures/ - pass-scenario fixture directory +# testdata/fixtures_fail/ - fail-scenario fixture directory declare -g TEST_PASS=0 declare -g TEST_FAIL=0 diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_report_json.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_report_json.sh index 703a6ace..0212d91f 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_report_json.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_report_json.sh @@ -1,6 +1,20 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_report_json.sh +# +# Tests for JSON report output structure and validity +# +# Validates emit_json_report produces well-formed JSON via python3's +# json.tool, contains required top-level fields (version, cis_benchmark, +# summary with score_percent), a sections array with all six CIS +# sections, and a controls array with the correct count of audited +# items. Also verifies file-based JSON output by writing to a tmpfile +# and validating the result. +# +# Connects to: +# lib/report_json.sh - emit_json_report under test +# lib/engine.sh - compute_scores (required before report) +# tests/test_helpers.sh - setup_test, assert_json_valid test_json_valid_output() { CURRENT_TEST="test_json_valid_output" diff --git a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_runner.sh b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_runner.sh index f82cfba6..0f6476ac 100755 --- a/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_runner.sh +++ b/PROJECTS/beginner/linux-cis-hardening-auditor/tests/test_runner.sh @@ -1,6 +1,23 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # test_runner.sh +# +# Test harness for the CIS auditor test suite +# +# Sources all project modules (constants, utils, registry, engine, +# report_json, baseline, registry_data, and all check files) plus the +# test_helpers assertion framework. Discovers test_*.sh files (excluding +# itself and test_helpers.sh), sources each one, introspects all +# functions matching the test_ prefix via declare -F, executes them in +# order, then cleans up by unsetting each test function. Supports +# running specific test files as CLI arguments or auto-discovering all +# files in the tests directory. Prints the aggregated pass/fail summary +# via print_results and exits non-zero on any failures. +# +# Connects to: +# tests/test_helpers.sh - assertion framework (setup_test, assert_*) +# all src/ modules - sourced to provide testable functions +# tests/test_*.sh - individual test files discovered and run set -euo pipefail diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/context.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/context.rs index 23008dd6..f409b03d 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/context.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/context.rs @@ -1,5 +1,26 @@ // ©AngelaMos | 2026 // context.rs +// +// Analysis context holding binary data and accumulated pass results +// +// BinarySource is a two-variant enum: Mapped wraps a memmap2 +// Mmap for disk-backed files, Buffered wraps an Arc<[u8]> for +// in-memory data received over the network. Both implement +// AsRef<[u8]> so passes access the binary through a uniform +// data() method. AnalysisContext is constructed with a source, +// SHA-256 digest, filename, and file size. Each analysis pass +// populates its corresponding Option field (format_result, +// import_result, string_result, entropy_result, +// disassembly_result, threat_result) as it runs, building up +// the full analysis incrementally. +// +// Connects to: +// formats/mod.rs - FormatResult +// passes/disasm.rs - DisassemblyResult +// passes/entropy.rs - EntropyResult +// passes/imports.rs - ImportResult +// passes/strings.rs - StringResult +// passes/threat.rs - ThreatResult use std::sync::Arc; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/error.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/error.rs index 4c0e368c..9ed12141 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/error.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/error.rs @@ -1,5 +1,17 @@ // ©AngelaMos | 2026 // error.rs +// +// Engine error type hierarchy +// +// EngineError is a thiserror enum covering all failure modes +// in the analysis pipeline: InvalidBinary for unparseable +// input, UnsupportedFormat and UnsupportedArchitecture for +// recognized but unhandled binaries, MissingDependency when +// a pass requires results from an earlier pass that did not +// run, PassFailed wrapping the source error from any pass, +// Yara for rule compilation or scan failures, and Io for +// filesystem operations. The From impl +// enables transparent propagation with the ? operator. #[derive(thiserror::Error, Debug)] pub enum EngineError { diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/elf.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/elf.rs index 7a2fe6ce..eec371df 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/elf.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/elf.rs @@ -1,5 +1,29 @@ // ©AngelaMos | 2026 // elf.rs +// +// ELF binary format parser +// +// Parses ELF binaries via goblin::elf into a FormatResult. +// Extracts architecture (x86/x86_64/ARM/AArch64), bitness, +// endianness, entry point, and checks for symbol table +// presence (stripped detection), PT_INTERP (PIE detection), +// and .debug_ sections. build_sections iterates section +// headers, computing SHA-256 per section and mapping +// SHF_ALLOC/SHF_WRITE/SHF_EXECINSTR flags to +// SectionPermissions. build_segments maps program headers +// with PF_R/PF_W/PF_X flags and named segment types. +// build_elf_info extracts OS ABI, ELF type, interpreter +// path, GNU_RELRO, stack executability, BIND_NOW (via +// DT_BIND_NOW and DF_BIND_NOW), and needed libraries. +// collect_function_hints gathers STT_FUNC symbol addresses +// for disassembly seeding. +// +// Connects to: +// formats/mod.rs - FormatResult, SectionInfo, SegmentInfo, +// ElfInfo, detect_common_anomalies, +// compute_section_hash +// types.rs - Architecture, BinaryFormat, Endianness, +// SectionPermissions use goblin::elf::dynamic::DT_BIND_NOW; use goblin::elf::header::{ diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/macho.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/macho.rs index 59a33e14..17a4c224 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/macho.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/macho.rs @@ -1,5 +1,30 @@ // ©AngelaMos | 2026 // macho.rs +// +// Mach-O binary format parser +// +// Parses Mach-O binaries via goblin::mach into a +// FormatResult. Handles both single-architecture and +// universal (fat) binaries by selecting the first valid +// architecture slice. Extracts CPU type (x86/x86_64/ARM/ +// ARM64), bitness, endianness, entry point, symbol +// presence (stripped detection), __DWARF segment (debug +// info), and MH_PIE flag. build_sections walks segments +// and their sections, mapping VM_PROT_* initprot flags to +// SectionPermissions with per-section SHA-256 hashes. +// build_macho_info scans load commands for CodeSignature, +// FunctionStarts, VersionMinMacosx, VersionMinIphoneos, +// BuildVersion, and dylib references. cpu_subtype_name +// decodes x86, ARM, and ARM64 subtypes. Function hints are +// collected from non-stab N_SECT symbols for disassembly +// seeding. +// +// Connects to: +// formats/mod.rs - FormatResult, MachOInfo, SectionInfo, +// SegmentInfo, detect_common_anomalies, +// compute_section_hash +// types.rs - Architecture, BinaryFormat, Endianness, +// SectionPermissions use goblin::mach::cputype::{ CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_X86, diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/mod.rs index a31a65f3..610a1867 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/mod.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/mod.rs @@ -1,5 +1,31 @@ // ©AngelaMos | 2026 // mod.rs +// +// Binary format parsing dispatcher and shared format types +// +// Dispatches binary data to the appropriate format parser +// (ELF, PE, or Mach-O) via goblin::Object::parse and +// returns a unified FormatResult. Defines all shared format +// types: SectionInfo and SegmentInfo with permissions and +// SHA-256 hashes, FormatAnomaly enum (entry point outside +// text, RWX sections, suspicious section names, empty names, +// virtual/raw size mismatches, overlay data, TLS callbacks, +// missing import tables, suspicious timestamps), +// format-specific info structs (PeInfo with DLL +// characteristics, ElfInfo with RELRO/BIND_NOW/stack +// executable flags, MachOInfo with code signature and dylib +// list). SUSPICIOUS_SECTION_NAMES maps 15 packer section +// names to their tool names. detect_common_anomalies runs +// cross-format structural checks on entry point placement, +// RWX permissions, section naming, and size ratios. +// +// Connects to: +// formats/elf.rs - parse_elf +// formats/pe.rs - parse_pe +// formats/macho.rs - parse_macho +// types.rs - Architecture, BinaryFormat, Endianness, +// SectionPermissions +// error.rs - EngineError mod elf; mod macho; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/pe.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/pe.rs index 67d2c281..226a7a88 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/pe.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/formats/pe.rs @@ -1,5 +1,29 @@ // ©AngelaMos | 2026 // pe.rs +// +// PE (Portable Executable) binary format parser +// +// Parses PE binaries via goblin::pe into a FormatResult. +// Extracts COFF machine type (i386/AMD64/ARM/ARM64), +// bitness, entry point, and optional header fields including +// image base, subsystem name, linker version, and DLL +// characteristics (ASLR, DEP, CFG, SEH, force integrity). +// build_sections maps PE sections with IMAGE_SCN_MEM_* +// permission flags and per-section SHA-256 hashes. +// detect_pe_anomalies flags zeroed, pre-1990, or post-2100 +// timestamps, TLS callback presence, empty import tables, +// and overlay data beyond the last section. detect_rich_header +// scans for the "Rich" signature in the DOS stub. Function +// hints are collected from PE export RVAs for disassembly +// seeding. +// +// Connects to: +// formats/mod.rs - FormatResult, PeInfo, +// PeDllCharacteristics, FormatAnomaly, +// SectionInfo, detect_common_anomalies, +// compute_section_hash +// types.rs - Architecture, BinaryFormat, Endianness, +// SectionPermissions use goblin::pe::PE; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/lib.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/lib.rs index ee0e2f16..9b828638 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/lib.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/lib.rs @@ -1,5 +1,28 @@ // ©AngelaMos | 2026 // lib.rs +// +// Analysis engine entry point and binary pipeline coordinator +// +// Declares the AnalysisEngine struct which wires together six +// analysis passes (format, imports, strings, entropy, disasm, +// threat) via the PassManager's topological execution order. +// analyze() takes raw binary bytes and a filename, computes a +// SHA-256 digest, constructs an AnalysisContext backed by an +// Arc'd buffer, runs all passes sequentially, and returns the +// populated context alongside a PassReport of per-pass +// outcomes. sha256_hex is re-exported for callers that need +// hashing without a full analysis run. +// +// Connects to: +// context.rs - AnalysisContext, BinarySource +// error.rs - EngineError +// pass.rs - PassManager, PassReport, AnalysisPass +// passes/format.rs - FormatPass +// passes/imports.rs - ImportPass +// passes/strings.rs - StringPass +// passes/entropy.rs - EntropyPass +// passes/disasm.rs - DisasmPass +// passes/threat.rs - ThreatPass pub mod context; pub mod error; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs index 98627fb5..5594abf7 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs @@ -1,5 +1,24 @@ // ©AngelaMos | 2026 // pass.rs +// +// Analysis pass trait, pass manager, and topological execution +// +// Defines the AnalysisPass trait (sealed via a private module +// to prevent external implementations) with name(), +// dependencies(), and run() methods. PassManager accepts a +// Vec of boxed passes, computes a topological execution order +// via Kahn's algorithm (panics on cycles), and run_all() +// executes them in dependency order, continuing through +// failures and recording each PassOutcome with timing and +// error info. PassReport aggregates outcomes with +// all_succeeded() and failed_passes() query methods. +// Includes unit tests using MockPass to verify topological +// ordering, diamond dependencies, failure continuation, +// cycle detection, and duration tracking. +// +// Connects to: +// context.rs - AnalysisContext (passed to each pass) +// error.rs - EngineError (returned by pass::run) use std::collections::{HashMap, VecDeque}; use std::time::Instant; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/disasm.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/disasm.rs index 790f2050..4eb68087 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/disasm.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/disasm.rs @@ -1,5 +1,45 @@ // ©AngelaMos | 2026 // disasm.rs +// +// Recursive descent disassembly and CFG construction pass +// +// DisasmPass depends on format and performs recursive +// descent disassembly of x86 and x86_64 binaries using +// the iced-x86 decoder with Intel syntax formatting. +// Non-x86 architectures receive an empty result. +// disassemble seeds a function queue from the entry point +// and format-provided function hints, then iterates +// through function addresses with caps of 1000 functions +// and 50000 total instructions. disassemble_function +// performs worklist-driven linear sweep within a single +// function, decoding instructions and tracking block +// leaders from branch targets and fallthroughs. +// Conditional branches split into taken/fallthrough +// successors, unconditional branches follow the target, +// and returns/interrupts terminate the block. Call +// instructions discover new function entry points added +// to the outer queue. build_basic_blocks partitions +// decoded instructions by block leaders and terminators, +// computing successor and predecessor edges. +// finalize_block determines successors from branch targets +// and fallthroughs. build_cfg emits CfgNode and CfgEdge +// structs with ConditionalTrue/ConditionalFalse/ +// Unconditional/Fallthrough edge types, limited to +// functions with 500 or fewer instructions. +// vaddr_to_offset translates virtual addresses to file +// offsets via section mappings. disassemble_code provides +// a standalone linear disassembly API. Unit tests verify +// simple function disassembly, basic block splitting on +// conditional branches, CFG edge generation, non-x86 +// empty results, ELF disassembly, and context population. +// +// Connects to: +// pass.rs - AnalysisPass trait, Sealed +// context.rs - AnalysisContext +// formats/mod.rs - SectionInfo +// types.rs - Architecture, CfgEdgeType, +// FlowControlType +// error.rs - EngineError use std::collections::{ BTreeMap, HashMap, HashSet, VecDeque, diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/entropy.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/entropy.rs index 423d2069..69ae98f9 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/entropy.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/entropy.rs @@ -1,5 +1,40 @@ // ©AngelaMos | 2026 // entropy.rs +// +// Shannon entropy analysis and packing detection pass +// +// EntropyPass depends on format and computes Shannon +// entropy for the overall binary and each section +// individually. shannon_entropy calculates bits-per-byte +// over a 256-bucket frequency distribution. +// classify_entropy maps values to five bands: Plaintext +// (<3.5), NativeCode (<6.0), Compressed (<7.0), Packed +// (<7.2), and Encrypted (>=7.2). Per-section analysis +// flags anomalies via EntropyFlag: HighEntropy (>7.0), +// HighVirtualToRawRatio (>10x), EmptyRawData (raw=0 with +// virtual>0), Rwx (read+write+execute permissions), and +// PackerSectionName. PACKER_SECTION_NAMES maps 15 known +// section names to packers: UPX (UPX0/1/2), Themida, +// VMProtect (.vmp0/1/2), ASPack (.aspack/.adata), +// PECompact (PEC2TO/PEC2/pec1), MPRESS (.MPRESS1/2), +// and Enigma (.enigma1/2). Structural packing indicators +// track empty-raw-with-executable-virtual sections and +// high virtual-to-raw ratios; two or more structural +// indicators trigger packing_detected. find_ep_section +// locates the entry point section and checks for PUSHAD +// (0x60) as the first byte, a classic packer stub marker. +// Unit tests verify zero-entropy, uniform distribution +// (~8.0 bits), empty data, classification thresholds, +// packer section name detection, high entropy flagging, +// UPX packer detection, ELF entropy analysis, and context +// population. +// +// Connects to: +// pass.rs - AnalysisPass trait, Sealed +// context.rs - AnalysisContext +// formats/mod.rs - SectionInfo +// types.rs - EntropyClassification, EntropyFlag +// error.rs - EngineError use serde::{Deserialize, Serialize}; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/format.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/format.rs index 628d4bd7..273ff2f8 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/format.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/format.rs @@ -1,5 +1,22 @@ // ©AngelaMos | 2026 // format.rs +// +// Format analysis pass (binary header parsing) +// +// FormatPass is the first pass in the pipeline with no +// dependencies. It delegates to formats::parse_format which +// dispatches to the ELF, PE, or Mach-O parser and stores the +// resulting FormatResult in the context. All subsequent passes +// depend on this pass for section layout, architecture, and +// entry point information. Unit tests verify ELF metadata +// extraction, section and segment presence, stripped binary +// detection, ELF info population, section hash computation, +// invalid binary rejection, and context population. +// +// Connects to: +// formats/mod.rs - parse_format, FormatResult +// pass.rs - AnalysisPass trait, Sealed +// context.rs - AnalysisContext use crate::context::AnalysisContext; use crate::error::EngineError; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/imports.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/imports.rs index 1a35d25e..e5d87b2c 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/imports.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/imports.rs @@ -1,5 +1,42 @@ // ©AngelaMos | 2026 // imports.rs +// +// Import/export table analysis pass +// +// ImportPass depends on format and extracts import tables, +// export tables, and linked library lists from ELF, PE, +// and Mach-O binaries via goblin. SUSPICIOUS_APIS defines +// 22 APIs tagged with MITRE ATT&CK technique IDs covering +// injection (T1055), process hollowing (T1055.012), APC +// injection (T1055.004), anti-debug (T1622), token +// manipulation (T1134), persistence (T1547.001, +// T1543.003), download (T1105), network (T1071), +// deobfuscation (T1140), and Linux-specific APIs (ptrace, +// mprotect, dlopen, dlsym, execve, process_vm_readv/ +// writev). SUSPICIOUS_COMBINATIONS defines 15 multi-API +// chain detections including Process Injection Chain, +// Process Hollowing, Credential Theft, APC/DLL Injection, +// Download and Execute, Registry/Service Persistence, and +// Linux-specific chains (ptrace injection, RWX memory, C2 +// connection, network listener, dynamic loading, process +// injection). matches_api handles Windows A/W suffix +// variants. extract_elf, extract_pe, and extract_mach +// dispatch to format-specific importers that populate +// ImportEntry with library, function, address, ordinal, +// and threat tags. detect_combinations matches import +// function names against CombinationDef patterns with +// deduplication. collect_mitre_mappings emits per-API +// MITRE technique mappings. Unit tests verify ELF import +// extraction, suspicious API flagging, combination +// detection for injection chains and A/W suffixes, false +// positive rejection, MITRE mapping collection, and +// context population. +// +// Connects to: +// pass.rs - AnalysisPass trait, Sealed +// context.rs - AnalysisContext +// types.rs - Severity +// error.rs - EngineError use std::collections::HashSet; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/mod.rs index cee27888..16356e85 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/mod.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/mod.rs @@ -1,5 +1,16 @@ // ©AngelaMos | 2026 // mod.rs +// +// Analysis pass module exports +// +// Re-exports the six analysis pass submodules: format +// (binary header parsing), imports (import/export table +// extraction and suspicious API detection), strings +// (ASCII/UTF-16LE extraction and categorization), entropy +// (Shannon entropy per section and packing detection), +// disasm (recursive descent disassembly with CFG +// construction), and threat (weighted scoring across all +// pass results plus YARA rule matching). pub mod disasm; pub mod entropy; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/strings.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/strings.rs index 71479d55..237a48ca 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/strings.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/strings.rs @@ -1,5 +1,45 @@ // ©AngelaMos | 2026 // strings.rs +// +// String extraction and categorization pass +// +// StringPass depends on format and extracts printable +// strings from raw binary data in both ASCII and UTF-16LE +// encodings with a minimum length of 4 characters. +// extract_ascii scans for contiguous runs of printable +// bytes (0x20-0x7E, tab, newline, CR), while +// extract_utf16le decodes little-endian wide character +// sequences terminated by null pairs. Each extracted +// string is classified into one of 14 StringCategory +// values by a priority-ordered classifier chain: Url +// (http/https/ftp prefixes), IpAddress (dotted quad +// validation), RegistryKey (HKEY_/HKLM/HKCU prefixes), +// ShellCommand (cmd.exe, powershell, /bin/sh indicators), +// PersistencePath (Run keys, cron, systemd, LaunchAgents), +// AntiAnalysis (VMware, VirtualBox, QEMU, debugger, Wine +// detection), PackerSignature (UPX!, MPRESS, Themida, +// VMProtect), SuspiciousApi (matched against the 22 +// SUSPICIOUS_APIS from imports.rs), DebugArtifact +// (/rustc/, .pdb, _ZN, DWARF), FilePath (Windows drive +// letters, UNC paths, Unix prefixes), CryptoWallet (BTC +// base58check and ETH 0x-prefixed addresses), Email +// (local@domain.tld validation), EncodedData (base64 +// character set with padding validation, minimum 20 +// chars), or Generic. Seven categories are flagged as +// suspicious. find_section attributes each string to its +// containing binary section by file offset. Statistics +// track totals by encoding and category. Unit tests +// verify minimum length filtering, UTF-16LE extraction, +// all 14 category classifiers, suspicious flag mapping, +// ELF string extraction, context population, and section +// attribution. +// +// Connects to: +// pass.rs - AnalysisPass trait, Sealed +// context.rs - AnalysisContext +// formats/mod.rs - SectionInfo +// passes/imports.rs - SUSPICIOUS_APIS +// types.rs - StringCategory, StringEncoding use std::collections::HashMap; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/threat.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/threat.rs index 531c7c94..7aea4450 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/threat.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/passes/threat.rs @@ -1,5 +1,61 @@ // ©AngelaMos | 2026 // threat.rs +// +// Weighted threat scoring and risk classification pass +// +// ThreatPass depends on all five preceding passes (format, +// imports, strings, entropy, disasm) and produces a +// composite threat score across eight capped scoring +// categories: Import/API Analysis (max 20), Entropy +// Analysis (max 15), Packing Indicators (max 15), String +// Analysis (max 10), Section Anomalies (max 10), Entry +// Point Anomalies (max 10), Anti-Analysis Indicators +// (max 10), and YARA Signature Matches (max 10). +// score_imports weights injection chains (15), hollowing +// chains (15), credential access (12), APC injection (8), +// anti-debug APIs (8), download/execute (6), persistence +// (7), very few imports (5), and Linux-specific chains +// (ptrace 8, RWX memory 5, C2 10, network listener 8, +// dynamic loading 5, process injection 15). +// score_entropy flags high-entropy sections (6 pts, cap 2) +// and very high overall entropy (3 pts). score_packing +// checks packer section names (5), signature matches (3), +// empty raw with virtual (4), high VR ratio (3), PUSHAD +// at EP (3), and modified UPX without magic string (5). +// score_strings checks C2 URL patterns with suspicious +// TLDs, shell commands, base64-encoded PE headers, registry +// persistence paths, and crypto wallet addresses. +// score_sections flags RWX sections (5), empty names (3), +// unusual section counts (2), and zero-size code (4). +// score_entry_point flags EP outside .text (5), EP in last +// section (5), EP outside all sections (7), and TLS +// callbacks (3). score_anti_analysis checks +// IsDebuggerPresent (3), NtQueryInformationProcess (5), VM +// detection strings (3), sandbox evasion (3), timing APIs +// (3), Linux ptrace checks (5), and /proc/self analysis +// (3). score_yara weights malware/critical rules (10), +// packer rules (3), and suspicious rules (5). classify_risk +// maps totals to five RiskLevel bands: Benign (0-15), Low +// (16-35), Medium (36-55), High (56-75), Critical (76+). +// MITRE technique mappings are deduplicated from import +// combinations and per-API mappings. generate_summary +// ranks the top 5 findings by points. Unit tests verify +// risk classification thresholds, category capping, empty +// scoring, summary generation, YARA malware/packer +// scoring, entropy scoring, RWX section scoring, and full +// context population through all predecessor passes. +// +// Connects to: +// pass.rs - AnalysisPass trait, Sealed +// context.rs - AnalysisContext +// formats/mod.rs - FormatResult, FormatAnomaly +// passes/imports.rs - ImportResult +// passes/strings.rs - StringResult +// passes/entropy.rs - EntropyResult +// yara.rs - YaraScanner, YaraMatch +// types.rs - RiskLevel, EntropyFlag, +// StringCategory +// error.rs - EngineError use std::collections::HashSet; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/types.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/types.rs index 61337cb1..1ce0ecde 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/types.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/types.rs @@ -1,5 +1,21 @@ // ©AngelaMos | 2026 // types.rs +// +// Core enum and struct definitions shared across all passes +// +// Defines the type vocabulary used throughout the engine: +// BinaryFormat (Elf/Pe/MachO), Architecture (x86 through +// AArch64 with an Other fallback), Endianness, RiskLevel +// (five tiers from Benign to Critical), Severity (four tiers +// for import threat tagging), StringEncoding (Ascii/Utf8/ +// Utf16Le), StringCategory (14 classifications from Url to +// Generic), EntropyClassification (five bands from Plaintext +// to Encrypted), EntropyFlag (five section anomaly markers), +// FlowControlType and CfgEdgeType for disassembly CFG +// representation, and SectionPermissions with an is_rwx() +// helper. All enums derive Serialize/Deserialize for JSON +// output and implement Display where needed for human- +// readable formatting. use serde::{Deserialize, Serialize}; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/yara.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/yara.rs index 39232cb2..469a6dce 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/yara.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/yara.rs @@ -1,5 +1,26 @@ // ©AngelaMos | 2026 // yara.rs +// +// YARA rule scanner with builtin detection rules +// +// Embeds 14 YARA rules as a compile-time constant covering +// UPX packing, anti-debugging (Windows and Linux), process +// injection, keylogger APIs, crypto mining, Windows and +// Linux persistence mechanisms, network backdoors, +// ransomware indicators, shellcode patterns (NOP sleds, egg +// hunters), obfuscation (XOR loops, base64 alphabet), C2 +// endpoint paths, and credential file access. YaraScanner +// wraps a compiled yara_x::Rules instance. new() compiles +// only the builtin ruleset; with_custom_rules() also loads +// .yar/.yara files from a directory. scan() executes against +// binary data and returns YaraMatch structs containing rule +// name, tags, metadata (description/category/severity), and +// matched string identifiers with counts. Unit tests verify +// compilation, UPX detection, process injection detection, +// and clean-data negative cases using fixture binaries. +// +// Connects to: +// error.rs - EngineError::Yara for compilation/scan failures use std::path::Path; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/integration.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/integration.rs index ab8d24f4..ed981bda 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/integration.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/tests/integration.rs @@ -1,5 +1,24 @@ // ©AngelaMos | 2026 // integration.rs +// +// Full pipeline integration tests +// +// Exercises the AnalysisEngine end-to-end against test +// fixture binaries. full_pipeline_elf loads hello_elf and +// asserts all six passes succeed, format result is ELF +// with non-empty sections, all context slots are populated, +// disassembly finds functions and instructions, and threat +// score has 8 categories capped at 100. +// full_pipeline_stripped_elf verifies stripped binary +// detection and full pipeline completion. +// sha256_computed_correctly asserts the context SHA-256 is +// a valid 64-character hex string. invalid_binary_handled +// feeds 4 bytes of garbage and asserts the format pass +// fails while the engine does not panic. +// +// Connects to: +// lib.rs - AnalysisEngine +// types.rs - BinaryFormat use axumortem_engine::types::BinaryFormat; use axumortem_engine::AnalysisEngine; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/config.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/config.rs index 1b3c60df..c6288a89 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/config.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/config.rs @@ -1,5 +1,20 @@ // ©AngelaMos | 2026 // config.rs +// +// Application configuration via CLI arguments and +// environment variables +// +// AppConfig derives clap::Parser to accept database_url, +// host (default 0.0.0.0), port (default 3000), +// max_upload_size (default 50 MiB), and cors_origin +// (default wildcard). Each field maps to both a --long +// flag and an environment variable. bind_address formats +// the host:port string for the TCP listener. +// +// Connects to: +// main.rs - parsed at startup +// state.rs - stored as Arc +// middleware/cors.rs - cors_origin read by layer() use clap::Parser; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/mod.rs index 3245184d..75be7c84 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/mod.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/mod.rs @@ -1,5 +1,16 @@ // ©AngelaMos | 2026 // mod.rs +// +// Database module exports and migration runner +// +// Re-exports the models and queries submodules. +// run_migrations executes embedded SQLx migrations from +// the ./migrations directory against the provided PgPool. +// +// Connects to: +// main.rs - called at startup +// db/models.rs - row and input structs +// db/queries.rs - SQL query functions pub mod models; pub mod queries; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/models.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/models.rs index 54483297..2cd314a8 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/models.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/models.rs @@ -1,5 +1,21 @@ // ©AngelaMos | 2026 // models.rs +// +// Database row and input structs +// +// AnalysisRow maps to the analyses table with UUID id, +// sha256 hash, file_name, file_size, format, architecture, +// entry_point, threat_score, risk_level, slug, and +// created_at timestamp. PassResultRow maps to the +// pass_results table with analysis_id foreign key, +// pass_name, JSON result blob, and duration_ms. +// NewAnalysis and NewPassResult are input structs for +// insert operations without server-generated fields. +// +// Connects to: +// db/queries.rs - used by insert and select queries +// routes/upload.rs - NewAnalysis built from engine output +// routes/analysis.rs - AnalysisRow returned to client use chrono::{DateTime, Utc}; use serde::Serialize; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/queries.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/queries.rs index 5f24d414..0d6714b0 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/queries.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/db/queries.rs @@ -1,5 +1,23 @@ // ©AngelaMos | 2026 // queries.rs +// +// PostgreSQL query functions +// +// find_slug_by_sha256 checks for an existing analysis by +// SHA-256 hash and returns the slug if cached. +// find_by_slug retrieves a full AnalysisRow by its +// URL-friendly slug. find_pass_results fetches all +// PassResultRow entries for an analysis_id ordered by +// pass_name. insert_analysis and insert_pass_result +// perform transactional inserts within a caller-provided +// Transaction, returning the created AnalysisRow and +// committing pass result rows respectively. +// +// Connects to: +// db/models.rs - AnalysisRow, PassResultRow, +// NewAnalysis, NewPassResult +// routes/upload.rs - insert_analysis, insert_pass_result +// routes/analysis.rs - find_by_slug, find_pass_results use sqlx::{PgPool, Postgres, Transaction}; use uuid::Uuid; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/error.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/error.rs index f2093514..b0246d7b 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/error.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/error.rs @@ -1,5 +1,20 @@ // ©AngelaMos | 2026 // error.rs +// +// API error types and HTTP response mapping +// +// ApiError enumerates six error variants: NoFile (400), +// FileTooLarge (400), InvalidBinary (400), +// AnalysisFailed (500), NotFound (404), and Internal +// (500). Each variant maps to a JSON response body with +// an error code string and human-readable message via the +// IntoResponse implementation. From impls convert +// sqlx::Error, serde_json::Error, and +// tokio::task::JoinError into Internal variants. +// +// Connects to: +// routes/upload.rs - returned from upload handler +// routes/analysis.rs - returned from analysis lookup use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/main.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/main.rs index 4eea8f91..99473535 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/main.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/main.rs @@ -1,5 +1,24 @@ // ©AngelaMos | 2026 // main.rs +// +// Axumortem web server entry point +// +// Bootstraps the Axum HTTP server with clap-driven CLI +// configuration, tracing subscriber initialization with +// EnvFilter, PostgreSQL connection pool (max 20 +// connections) via SQLx PgPoolOptions, automatic database +// migrations, and AnalysisEngine initialization. Assembles +// AppState from the pool, engine, and config, then applies +// tower layers for HTTP tracing, CORS, and body size +// limits before binding a TCP listener. Graceful shutdown +// is handled via ctrl_c signal. +// +// Connects to: +// config.rs - AppConfig (clap Parser) +// state.rs - AppState +// db/mod.rs - run_migrations +// middleware/ - cors::layer +// routes/mod.rs - api_router mod config; mod db; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/cors.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/cors.rs index e0dff4ff..63a41a8b 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/cors.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/cors.rs @@ -1,5 +1,17 @@ // ©AngelaMos | 2026 // cors.rs +// +// CORS middleware configuration +// +// layer() builds a tower-http CorsLayer allowing GET, +// POST, and OPTIONS methods with Content-Type and Accept +// headers. When cors_origin is "*" the layer permits any +// origin; otherwise it parses the configured origin string +// into a single allowed HeaderValue. +// +// Connects to: +// config.rs - AppConfig.cors_origin +// main.rs - applied as tower layer use axum::http::header::{HeaderName, ACCEPT, CONTENT_TYPE}; use axum::http::Method; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/mod.rs index 9c449d6a..e443c20f 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/mod.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/middleware/mod.rs @@ -1,4 +1,9 @@ // ©AngelaMos | 2026 // mod.rs +// +// Middleware module exports +// +// Re-exports the cors submodule which provides the +// tower-http CORS layer configured from AppConfig. pub mod cors; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/analysis.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/analysis.rs index d5246276..9f853ae1 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/analysis.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/analysis.rs @@ -1,5 +1,19 @@ // ©AngelaMos | 2026 // analysis.rs +// +// Analysis result retrieval endpoint +// +// get_by_slug extracts the slug path parameter, queries +// the analysis row by slug, fetches all associated pass +// result rows, and assembles an AnalysisResponse with +// metadata fields and a passes HashMap mapping pass names +// to their JSON result blobs. Returns 404 via +// ApiError::NotFound if the slug does not exist. +// +// Connects to: +// state.rs - AppState +// db/queries.rs - find_by_slug, find_pass_results +// error.rs - ApiError use std::collections::HashMap; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/health.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/health.rs index 7d25fc89..dd308cb7 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/health.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/health.rs @@ -1,5 +1,15 @@ // ©AngelaMos | 2026 // health.rs +// +// Health check endpoint +// +// check executes a SELECT 1 probe against the PostgreSQL +// pool and returns a JSON HealthResponse with status "ok" +// and database connectivity as "connected" or +// "disconnected". +// +// Connects to: +// state.rs - AppState.db use axum::extract::State; use axum::Json; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/mod.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/mod.rs index 2ef0d4d2..a085ed3a 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/mod.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/mod.rs @@ -1,5 +1,18 @@ // ©AngelaMos | 2026 // mod.rs +// +// Route module exports and API router construction +// +// api_router assembles the Axum Router with three +// endpoints: GET /api/health (health check), POST +// /api/upload (binary upload and analysis), and GET +// /api/analysis/{slug} (analysis result retrieval). +// +// Connects to: +// routes/health.rs - check handler +// routes/upload.rs - handle handler +// routes/analysis.rs - get_by_slug handler +// state.rs - AppState mod analysis; mod health; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/upload.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/upload.rs index d1f8f634..cef05ec4 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/upload.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/routes/upload.rs @@ -1,5 +1,28 @@ // ©AngelaMos | 2026 // upload.rs +// +// Binary upload and analysis endpoint +// +// handle accepts a multipart file upload, computes +// SHA-256, and checks for a cached analysis by hash via +// find_slug_by_sha256. On cache miss it spawns +// AnalysisEngine::analyze on a blocking thread, builds a +// NewAnalysis from the format and threat results, generates +// a 12-character slug from the SHA-256 prefix, and +// transactionally inserts the analysis row and all six +// pass result rows. build_pass_results serializes each +// context field (format, imports, strings, entropy, +// disassembly, threat) to JSON with duration metadata. +// PASS_NAME_MAP renames "disasm" to "disassembly" for the +// API. extract_file iterates multipart fields looking for +// the "file" field name. +// +// Connects to: +// state.rs - AppState (engine, db, config) +// db/queries.rs - find_slug_by_sha256, insert_analysis, +// insert_pass_result +// db/models.rs - NewAnalysis, NewPassResult +// error.rs - ApiError use std::collections::HashMap; use std::sync::Arc; diff --git a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/state.rs b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/state.rs index 936f3f7f..289d12d9 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/state.rs +++ b/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem/src/state.rs @@ -1,5 +1,17 @@ // ©AngelaMos | 2026 // state.rs +// +// Shared application state for Axum handlers +// +// AppState holds the SQLx PgPool for database access, an +// Arc-wrapped AnalysisEngine for binary analysis, and an +// Arc-wrapped AppConfig. Derives Clone for Axum's State +// extractor. +// +// Connects to: +// main.rs - constructed at startup +// config.rs - AppConfig +// routes/ - extracted via State use std::sync::Arc; diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/App.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/App.tsx index a7d82d6a..0773157d 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/App.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/App.tsx @@ -1,6 +1,15 @@ // =========================== // ©AngelaMos | 2026 // App.tsx +// +// Root application component that wraps the router with +// TanStack Query provider, Sonner toast notifications +// (dark theme, top-right, 2s duration), and React Query +// devtools +// +// Connects to: +// core/api - queryClient +// core/app - router // =========================== import { QueryClientProvider } from '@tanstack/react-query' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/hooks/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/hooks/index.ts index 383e6e60..05fd0cb7 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/hooks/index.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/hooks/index.ts @@ -1,6 +1,28 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// TanStack React Query hooks for binary upload and +// analysis retrieval +// +// useUpload returns a mutation that POSTs a File as +// multipart/form-data to API_ENDPOINTS.UPLOAD with +// UPLOAD_TIMEOUT_MS (120s), Zod-validates the response +// through UploadResponseSchema, and transforms Axios +// errors via transformAxiosError. useAnalysis returns a +// query keyed by QUERY_KEYS.ANALYSIS.BY_SLUG(slug) +// that GETs the full analysis result, Zod-validates +// through AnalysisResponseSchema, and is configured +// with staleTime: Infinity and no window-focus refetch +// since analysis results are immutable once computed +// +// Connects to: +// config.ts - API_ENDPOINTS, QUERY_KEYS, +// UPLOAD_TIMEOUT_MS +// core/api/api.config - apiClient instance +// core/api/errors - transformAxiosError +// api/schemas - parse() validation +// api/types - UploadResponse, ApiErrorBody // =================== import { useMutation, useQuery } from '@tanstack/react-query' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/index.ts index 8c2d64e5..784aafcd 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/index.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/index.ts @@ -1,6 +1,9 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// API layer barrel export for hooks, Zod schemas, and +// inferred TypeScript types // =================== export * from './hooks' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/schemas.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/schemas.ts index ad691b07..73984a5c 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/schemas.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/schemas.ts @@ -1,6 +1,38 @@ // =================== // © AngelaMos | 2026 // schemas.ts +// +// Zod runtime validation schemas mirroring every Rust +// engine result type +// +// Defines ~40 Zod schemas that map one-to-one with the +// backend serde output: enum schemas for BinaryFormat, +// Architecture, Endianness, RiskLevel, Severity, +// StringEncoding, StringCategory (14 variants), +// EntropyClassification (5 bands), EntropyFlag, +// FlowControlType, and CfgEdgeType; object schemas for +// SectionInfo, SegmentInfo, PeInfo, ElfInfo, MachOInfo +// (format pass), ImportEntry, ExportEntry, Suspicious +// Combination, ImportMitreMapping, ImportStatistics +// (import pass), ExtractedString, StringStatistics +// (string pass), SectionEntropy, PackingIndicator +// (entropy pass), InstructionInfo, BasicBlockInfo, +// CfgNode, CfgEdge, FunctionCfg, FunctionInfo +// (disassembly pass), ScoringDetail, ScoringCategory, +// ThreatMitreMapping, YaraMetadata, YaraStringMatch, +// YaraMatch (threat pass); and top-level composite +// schemas FormatResult, ImportResult, StringResult, +// EntropyResult, DisassemblyResult, ThreatResult, +// AnalysisPasses (all six optional), AnalysisResponse, +// UploadResponse, and ApiErrorBody. Every API response +// is parsed through these schemas before reaching +// components +// +// Connects to: +// api/types - z.infer exports for each schema +// api/hooks - AnalysisResponseSchema, UploadResponse +// Schema used in parse() calls +// Rust types - mirrors types.rs serde output exactly // =================== import { z } from 'zod' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/types/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/types/index.ts index 37c7bd75..dc5876ea 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/types/index.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/api/types/index.ts @@ -1,6 +1,36 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// Inferred TypeScript types derived from Zod schemas +// via z.infer +// +// Exports ~45 types covering every domain model in the +// analysis pipeline: enums (BinaryFormat, Architecture, +// Endianness, RiskLevel, Severity, StringEncoding, +// StringCategory, EntropyClassification, EntropyFlag, +// FlowControlType, CfgEdgeType), format structures +// (SectionPermissions, SectionInfo, SegmentInfo, +// FormatAnomaly, PeDllCharacteristics, PeInfo, ElfInfo, +// MachOInfo, FormatResult), import structures (Import +// Entry, ExportEntry, SuspiciousCombination, Import +// MitreMapping, ImportStatistics, ImportResult), string +// structures (ExtractedString, StringStatistics, +// StringResult), entropy structures (SectionEntropy, +// PackingIndicator, EntropyResult), disassembly +// structures (InstructionInfo, BasicBlockInfo, CfgNode, +// CfgEdge, FunctionCfg, FunctionInfo, Disassembly +// Result), threat structures (ScoringDetail, Scoring +// Category, ThreatMitreMapping, YaraMetadata, Yara +// StringMatch, YaraMatch, ThreatResult), and top-level +// composites (AnalysisPasses, AnalysisResponse, Upload +// Response, ApiErrorBody) +// +// Connects to: +// api/schemas - source schemas for z.infer +// api/hooks - UploadResponse, ApiErrorBody +// pages/ - all analysis result types consumed +// by tab components // =================== import type { z } from 'zod' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/config.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/config.ts index 58faf704..a38992a5 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/config.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/config.ts @@ -1,6 +1,21 @@ // =================== // © AngelaMos | 2026 // config.ts +// +// Centralized application constants including API +// endpoint paths, TanStack Query cache keys, route +// definitions, localStorage keys, query timing +// strategies (stale/gc/retry), HTTP status codes, +// upload timeout (120s), and color maps for +// RiskLevel (5 levels) and EntropyClassification +// (5 bands) +// +// Connects to: +// api/hooks - API_ENDPOINTS, QUERY_KEYS +// core/app - ROUTES +// core/api - QUERY_CONFIG +// pages/ - RISK_LEVEL_COLORS, +// ENTROPY_CLASSIFICATION_COLORS // =================== export const API_ENDPOINTS = { diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/api.config.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/api.config.ts index f48a4d08..a4ed98ef 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/api.config.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/api.config.ts @@ -1,6 +1,13 @@ // =================== // © AngelaMos | 2026 // api.config.ts +// +// Axios HTTP client instance configured with base URL +// from VITE_API_URL (fallback /api), 15s timeout, JSON +// content type, and credentials enabled +// +// Connects to: +// api/hooks - used for upload and analysis requests // =================== import axios, { type AxiosInstance } from 'axios' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/errors.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/errors.ts index 492672f6..a929d746 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/errors.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/errors.ts @@ -1,6 +1,20 @@ /** * ©AngelaMos | 2026 * errors.ts + * + * API error classification and Axios error transformer + * + * ApiError extends Error with a typed code (9 variants + * from NETWORK_ERROR to UNKNOWN_ERROR), HTTP status code, + * optional validation details, and getUserMessage() for + * user-facing strings. transformAxiosError maps HTTP + * status codes to ApiErrorCode values and extracts + * detail/message from response bodies. Registers ApiError + * as the TanStack React Query default error type. + * + * Connects to: + * core/api/query.config.ts - error handling in caches + * api/hooks - onError transforms */ import type { AxiosError } from 'axios' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/index.ts index f6dc3639..f4b6f9d5 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/index.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/index.ts @@ -1,6 +1,9 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// Core API barrel export for apiClient, error types, +// query client, and query strategies // =================== export * from './api.config' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/query.config.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/query.config.ts index 42084ec0..c80ee1ed 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/query.config.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/api/query.config.ts @@ -1,6 +1,17 @@ // =================== // © AngelaMos | 2026 // query.config.ts +// +// TanStack React Query client configuration with retry +// logic (exponential backoff, skip for auth/404/validation +// errors), Sonner toast integration for background query +// and mutation cache errors, and four pre-built query +// strategies (standard, frequent, static, auth) +// +// Connects to: +// config.ts - QUERY_CONFIG timing constants +// errors.ts - ApiError, ApiErrorCode +// App.tsx - QueryClientProvider // =================== import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/routers.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/routers.tsx index 89583966..f1b2a3f3 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/routers.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/routers.tsx @@ -1,6 +1,22 @@ // =================== // © AngelaMos | 2026 // routers.tsx +// +// Browser router definition with lazy-loaded routes +// wrapped in a Shell layout +// +// Declares three route entries under a single Shell +// parent: ROUTES.HOME loads the landing page, ROUTES +// .ANALYSIS loads the analysis results page, and a +// wildcard catch-all falls back to landing. Both page +// components use React.lazy via react-router-dom's +// lazy() convention for code-split chunk loading +// +// Connects to: +// config.ts - ROUTES path constants +// shell.tsx - Shell layout wrapper +// pages/landing - lazy-loaded upload page +// pages/analysis - lazy-loaded results page // =================== import { createBrowserRouter, type RouteObject } from 'react-router-dom' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.tsx index 6bfd8518..46160c56 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/app/shell.tsx @@ -1,6 +1,24 @@ // =================== // © AngelaMos | 2026 // shell.tsx +// +// Root application shell with error boundary and +// suspense wrapper around the router outlet +// +// Shell renders a full-page layout container with an +// ErrorBoundary (ShellErrorFallback displays the error +// message) wrapping a Suspense boundary (ShellLoading +// shows a spinner placeholder) around the react-router +// Outlet. All lazy-loaded page components resolve +// through this boundary pair, ensuring both loading +// states and uncaught render errors are handled at the +// top level +// +// Connects to: +// routers.tsx - mounted as parent route element +// shell.module.scss - shell, content, error, loading +// layout styles +// pages/ - rendered via Outlet // =================== import { Suspense } from 'react' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/format.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/format.ts index 3eb999d0..f99b7400 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/format.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/format.ts @@ -1,6 +1,22 @@ // =================== // © AngelaMos | 2026 // format.ts +// +// Display formatting utilities for binary analysis +// values +// +// formatBytes converts raw byte counts to human-readable +// strings (B/KB/MB/GB) using 1024-based units with two +// decimal places above bytes. formatHex renders numbers +// as zero-padded uppercase hex strings (default 8 chars). +// truncateHash shortens SHA-256 digests to a display +// length (default 16 chars) with an ellipsis. copyTo +// Clipboard wraps the Clipboard API with a boolean +// success/failure return +// +// Connects to: +// pages/analysis - hex formatting, hash display +// pages/landing - byte size display // =================== const BYTE_UNITS = ['B', 'KB', 'MB', 'GB'] as const diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/index.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/index.ts index 58f981ae..ab0d302d 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/index.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/index.ts @@ -1,6 +1,9 @@ // =================== // © AngelaMos | 2026 // index.ts +// +// Core library barrel export for formatting utilities +// and UI state store // =================== export * from './format' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/shell.ui.store.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/shell.ui.store.ts index e3005782..998641b5 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/shell.ui.store.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/core/lib/shell.ui.store.ts @@ -1,6 +1,23 @@ /** * ©AngelaMos | 2026 * shell.ui.store.ts + * + * Zustand UI state store with devtools and localStorage + * persistence + * + * Manages global theme (light/dark/system), sidebar open + * state, and sidebar collapsed state through a single + * Zustand store wrapped in devtools (named "UIStore" for + * Redux DevTools inspection) and persist middleware that + * serializes theme and sidebarCollapsed to localStorage + * under the "ui-storage" key. Exports three selector + * hooks (useTheme, useSidebarOpen, useSidebarCollapsed) + * for granular subscriptions without re-renders + * + * Connects to: + * config.ts - STORAGE_KEYS.UI matches persist key + * shell.tsx - consumes sidebar/theme state + * pages/ - theme-aware rendering */ import { create } from 'zustand' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/main.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/main.tsx index ac319510..258d0069 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/main.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/main.tsx @@ -1,6 +1,14 @@ // =========================== // ©AngelaMos | 2026 // main.tsx +// +// Application entry point that mounts the React root +// into the #root DOM element with StrictMode enabled +// and imports the global SCSS stylesheet +// +// Connects to: +// App.tsx - root component +// styles.scss - global styles // =========================== import { StrictMode } from 'react' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/index.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/index.tsx index 1448819b..122d75c7 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/index.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/index.tsx @@ -1,6 +1,41 @@ // =================== // © AngelaMos | 2026 // index.tsx +// +// Analysis results page with threat score card, tab +// navigation, and six analysis tab panels +// +// Fetches the full analysis via useAnalysis(slug) from +// the URL params and renders three main sections: a +// header with file name, format/architecture/size +// badges, SHA-256 copy-to-clipboard button, and entry +// point hex display; a threat score card with numeric +// score colored by RISK_LEVEL_COLORS, risk label, and +// per-category ScoreBar components showing score/max +// fill percentages; and a six-tab navigation bar +// (overview, headers, imports, strings, entropy, +// disassembly) that switches between TabOverview, +// TabHeaders, TabImports, TabStrings, TabEntropy, and +// TabDisassembly via renderTab dispatch. Shows loading +// and 404 states with a back link to ROUTES.HOME. +// Lazy-loaded via react-router with displayName +// "Analysis" +// +// Connects to: +// api/hooks - useAnalysis query +// api/types - AnalysisResponse +// config.ts - RISK_LEVEL_COLORS, ROUTES +// core/lib - copyToClipboard, formatBytes, +// formatHex, truncateHash +// tab-overview.tsx - TabOverview component +// tab-headers.tsx - TabHeaders component +// tab-imports.tsx - TabImports component +// tab-strings.tsx - TabStrings component +// tab-entropy.tsx - TabEntropy component +// tab-disassembly.tsx - TabDisassembly component +// analysis.module +// .scss - all layout styles +// routers.tsx - lazy-loaded at ROUTES.ANALYSIS // =================== import { useState } from 'react' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-disassembly.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-disassembly.tsx index 3f4eeba0..952e71f3 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-disassembly.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-disassembly.tsx @@ -1,6 +1,38 @@ // =================== // © AngelaMos | 2026 // tab-disassembly.tsx +// +// Disassembly tab with function sidebar, instruction +// table, and dagre-layouted control flow graph +// +// Renders a two-panel layout: a left sidebar listing +// all disassembled functions (address, name or sub_hex +// fallback, instruction count) with entry point +// highlighting and click-to-select via selectedAddr +// state; and a main panel showing the selected +// function's header (name, address, size, instruction +// count, block count), an InstructionTable with per- +// basic-block rows (address, hex bytes, mnemonic, +// operands) with block boundary markers, and a CfgGraph +// SVG visualization. layoutCfg uses @dagrejs/dagre for +// top-to-bottom hierarchical layout with CFG_NODE_WIDTH +// (160), CFG_NODE_HEIGHT (40), CFG_RANK_SEP (60), and +// CFG_NODE_SEP (30). CfgGraph renders nodes as labeled +// rectangles and edges as colored lines with arrowhead +// markers: Fallthrough gray, ConditionalTrue green, +// ConditionalFalse red, Unconditional blue, Call purple +// +// Connects to: +// api/types - AnalysisResponse, CfgEdge, +// CfgEdgeType, CfgNode, +// FunctionInfo +// core/lib - formatHex +// @dagrejs/dagre - Graph, layout for CFG +// positioning +// analysis/index - mounted in renderTab switch +// analysis.module +// .scss - disasmLayout, fnSidebar, +// cfgContainer, cfgSvg styles // =================== import { layout as dagreLayout, Graph } from '@dagrejs/dagre' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-entropy.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-entropy.tsx index bad93d50..9a05f364 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-entropy.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-entropy.tsx @@ -1,6 +1,32 @@ // =================== // © AngelaMos | 2026 // tab-entropy.tsx +// +// Entropy tab with per-section entropy bars, packing +// detection alert, and classification coloring +// +// Displays overall entropy as a prominent value out of +// MAX_ENTROPY (8.0). When packing is detected, renders +// a packing alert with packer name and indicator list +// (type and description for each). Per-section entropy +// is shown as horizontal bars where fill width is +// entropy/8 percentage and color is mapped from +// ENTROPY_CLASSIFICATION_COLORS (Plaintext green, +// NativeCode blue, Compressed yellow, Packed orange, +// Encrypted red). Each EntropyBar shows section name, +// classification label, bar visualization, numeric +// entropy value, size, virtual-to-raw ratio, and any +// EntropyFlag badges (HighEntropy, Rwx, Packer +// SectionName, etc.), with anomalous sections +// highlighted +// +// Connects to: +// api/types - AnalysisResponse, SectionEntropy +// config.ts - ENTROPY_CLASSIFICATION_COLORS +// analysis/index - mounted in renderTab switch +// analysis.module +// .scss - entropyRow, entropyBarFill, +// packingAlert styles // =================== import type { AnalysisResponse, SectionEntropy } from '@/api' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-headers.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-headers.tsx index 8409cbdf..913cfbc8 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-headers.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-headers.tsx @@ -1,6 +1,31 @@ // =================== // © AngelaMos | 2026 // tab-headers.tsx +// +// Headers tab displaying binary format metadata, +// format-specific info, section and segment tables +// +// Renders a format info grid (format, arch, bits, +// endianness, entry point, stripped/PIE/debug flags), +// then conditionally shows PE info (image base, +// subsystem, linker version, ASLR/DEP/CFG), ELF info +// (OS ABI, type, RELRO, bind-now, NX stack, needed +// libraries list), or Mach-O info (file type, universal, +// code signature). Below, a sections table shows name, +// virtual address (formatHex), virtual size, raw size, +// and R/W/X permissions via PermsBadge (with execute +// highlighted in a distinct style). Segments are in a +// collapsible section toggled by showSegments state, +// showing name, vaddr, vsize, fsize, and permissions +// +// Connects to: +// api/types - AnalysisResponse, SectionInfo, +// SegmentInfo +// core/lib - formatHex +// analysis/index - mounted in renderTab switch +// analysis.module +// .scss - metaGrid, dataTable, perm +// styles // =================== import { useState } from 'react' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-imports.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-imports.tsx index 174a3012..b13d4697 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-imports.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-imports.tsx @@ -1,6 +1,30 @@ // =================== // © AngelaMos | 2026 // tab-imports.tsx +// +// Imports tab with library-grouped import tables, +// suspicious combination alerts, and export listing +// +// Groups imports by library into collapsible Library +// Group sections (tracked via openLibs Set state), +// each showing function name, hex address, ordinal, +// and threat tags with suspicious row highlighting via +// ImportRow. Above the import groups, suspicious API +// combinations render as alert cards with name, MITRE +// ID pill, severity badge (styled per level), description, +// and matched API tags. Below, an exports section shows +// name, address, ordinal, and forward target in a +// standard data table. All addresses formatted via +// formatHex, with PAGE_SIZE-less full rendering since +// import counts are typically manageable +// +// Connects to: +// api/types - AnalysisResponse, ImportEntry +// core/lib - formatHex +// analysis/index - mounted in renderTab switch +// analysis.module +// .scss - libraryGroup, alertCard, +// dataTable, severityBadge styles // =================== import { useState } from 'react' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-overview.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-overview.tsx index 77ae4a28..b813683d 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-overview.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-overview.tsx @@ -1,6 +1,29 @@ // =================== // © AngelaMos | 2026 // tab-overview.tsx +// +// Overview tab showing summary cards for all six +// analysis passes, anomalies list, and MITRE ATT&CK +// technique links +// +// Renders a six-card summary grid: format (type, bits, +// section/segment counts), imports (total across +// libraries, suspicious count), strings (total +// extracted, suspicious count), entropy (overall value, +// packing detection status), disassembly (function and +// instruction totals), and YARA (rule match count with +// summary text). Below the grid, displays format +// anomalies as string or key-value entries, and MITRE +// ATT&CK techniques as clickable pill links that open +// attack.mitre.org technique pages (with sub-technique +// slash formatting via formatMitreUrl) +// +// Connects to: +// api/types - AnalysisResponse +// analysis/index - mounted in renderTab switch +// analysis.module +// .scss - summaryGrid, summaryCard, +// anomalyList, mitrePills styles // =================== import type { AnalysisResponse } from '@/api' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx index f7a34385..41748dff 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/analysis/tab-strings.tsx @@ -1,6 +1,32 @@ // =================== // © AngelaMos | 2026 // tab-strings.tsx +// +// Strings tab with search, encoding/category filters, +// pagination, and expandable string values +// +// Provides a filter bar with text search, encoding +// dropdown (All/Ascii/Utf8/Utf16Le), category dropdown +// (All plus 14 StringCategory values), and a suspicious +// -only toggle. Filters apply via useMemo over the full +// string array, with results paginated at PAGE_SIZE (50) +// and displayed in a table showing hex offset, value +// (truncated at 80 chars with expand toggle tracked via +// expandedRows Set), encoding, category badge, and +// section name. StringRow highlights suspicious entries +// and supports click-to-expand for long values. Prev/ +// Next pagination controls appear when totalPages +// exceeds one +// +// Connects to: +// api/types - AnalysisResponse, Extracted +// String, StringCategory, +// StringEncoding +// core/lib - formatHex +// analysis/index - mounted in renderTab switch +// analysis.module +// .scss - filterBar, searchInput, +// pagination, dataTable styles // =================== import { useMemo, useState } from 'react' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/landing/index.tsx b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/landing/index.tsx index 030bd735..7dc12d35 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/landing/index.tsx +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/src/pages/landing/index.tsx @@ -1,6 +1,31 @@ // =================== // © AngelaMos | 2026 // index.tsx +// +// Binary upload landing page with drag-and-drop file +// intake and analysis pipeline visualization +// +// Renders the Axumortem specimen intake interface: an +// animated SVG grain background, hex offset margin +// decoration (16 addresses), corner brackets, meta +// strip header, and format support badges (ELF/PE/ +// Mach-O). The drop zone supports both drag-and-drop +// and click-to-browse file selection, displaying file +// name, size (via formatBytes), and MIME type once +// selected. On submit, useUpload posts the binary as +// multipart/form-data and navigates to /analysis/:slug +// on success. A six-step pipeline visualization shows +// the FORMAT through THREAT analysis passes. The +// Component is lazy-loaded via react-router and +// exported with displayName "Landing" +// +// Connects to: +// api/hooks - useUpload mutation +// core/lib - formatBytes +// config.ts - implicit via useUpload endpoints +// landing.module +// .scss - all layout and animation styles +// routers.tsx - lazy-loaded at ROUTES.HOME // =================== import { useCallback, useRef, useState } from 'react' diff --git a/PROJECTS/intermediate/binary-analysis-tool/frontend/vite.config.ts b/PROJECTS/intermediate/binary-analysis-tool/frontend/vite.config.ts index a8d5d478..fa79b248 100644 --- a/PROJECTS/intermediate/binary-analysis-tool/frontend/vite.config.ts +++ b/PROJECTS/intermediate/binary-analysis-tool/frontend/vite.config.ts @@ -1,6 +1,26 @@ /** - * ©AngelaMos | 2025 + * ©AngelaMos | 2026 * vite.config.ts + * + * Vite build configuration with React plugin, path + * aliases, dev proxy, SCSS preprocessing, and manual + * chunk splitting + * + * Configures @vitejs/plugin-react and vite-tsconfig-paths + * plugins, resolves @ alias to ./src, enables SCSS + * preprocessing, and sets up a dev server on port 5173 + * with /api proxy to VITE_API_TARGET (fallback localhost + * :8000) that strips the /api prefix. Production builds + * target esnext with oxc minification, hidden sourcemaps, + * and manual chunks splitting react-dom/react-router into + * vendor-react, @tanstack/react-query into vendor-query, + * and zustand into vendor-state. Environment variables are + * loaded from the parent directory via loadEnv + * + * Connects to: + * src/App.tsx - root application component + * src/config.ts - VITE_API_URL consumed at runtime + * tsconfig.json - path aliases resolved by plugin */ import path from 'node:path' diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/apptoken.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/apptoken.nim index 933d0ad4..8240af3e 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/apptoken.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/apptoken.nim @@ -1,5 +1,32 @@ # ©AngelaMos | 2026 # apptoken.nim +# +# Application token and database credential collector +# +# Scans for credential exposure across desktop apps, databases, +# package registries, and infrastructure tools. Checks application +# data directories (Slack, Discord, VS Code). scanDbCredFiles +# inspects database credential files: .pgpass (PostgreSQL entry +# count), .my.cnf (MySQL password presence), .rediscli_auth (Redis), +# and .mongorc.js (MongoDB auth). scanDockerConfig checks +# .docker/config.json for registry authentication tokens. scanNetrc +# parses .netrc for machine entries with passwords. scanDevTokenFiles +# checks .npmrc for _authToken, .pypirc for passwords, and +# .config/gh/hosts.yml for GitHub CLI OAuth tokens. +# scanInfraTokenFiles checks Terraform Cloud credentials, Vault +# tokens, Helm repository passwords, and Rclone cloud storage +# configs. Severity escalates for world-readable files and files +# containing plaintext credentials. +# +# Connects to: +# collectors/base.nim - expandHome, safeFileExists, safeDirExists, +# readFileContent, readFileLines, isWorldReadable, +# isGroupReadable, makeFinding, makeFindingWithCred, +# permissionSeverity +# config.nim - PgPass, MyCnf, RedisConf, MongoRc, DockerConfig, +# NetrcFile, NpmrcFile, PypircFile, GhCliHosts, +# TerraformCreds, VaultTokenFile, HelmRepos, +# RcloneConf, SlackDir, DiscordDir, VsCodeDir {.push raises: [].} @@ -8,39 +35,30 @@ import ../types import ../config import base -type - AppTarget = object - path: string - name: string - description: string - isDir: bool - -proc scanAppDir( - config: HarvestConfig, - target: AppTarget, - result: var CollectorResult -) = +type AppTarget = object + path: string + name: string + description: string + isDir: bool + +proc scanAppDir(config: HarvestConfig, target: AppTarget, result: var CollectorResult) = let fullPath = expandHome(config, target.path) if target.isDir: if not safeDirExists(fullPath): return let sev = permissionSeverity(fullPath, isDir = true) - result.findings.add(makeFinding( - fullPath, - target.description, - catApptoken, sev - )) + result.findings.add(makeFinding(fullPath, target.description, catApptoken, sev)) else: if not safeFileExists(fullPath): return - let sev = if isWorldReadable(fullPath): svCritical - elif isGroupReadable(fullPath): svHigh - else: svMedium - result.findings.add(makeFinding( - fullPath, - target.description, - catApptoken, sev - )) + let sev = + if isWorldReadable(fullPath): + svCritical + elif isGroupReadable(fullPath): + svHigh + else: + svMedium + result.findings.add(makeFinding(fullPath, target.description, catApptoken, sev)) proc scanDbCredFiles(config: HarvestConfig, result: var CollectorResult) = let pgpassPath = expandHome(config, PgPass) @@ -57,53 +75,69 @@ proc scanDbCredFiles(config: HarvestConfig, result: var CollectorResult) = source: pgpassPath, credType: "postgresql_credentials", preview: $entryCount & " database connection entries", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("entry_count", $entryCount) - result.findings.add(makeFindingWithCred( - pgpassPath, - "PostgreSQL password file with " & $entryCount & " entries", - catApptoken, sev, cred - )) + result.findings.add( + makeFindingWithCred( + pgpassPath, + "PostgreSQL password file with " & $entryCount & " entries", + catApptoken, + sev, + cred, + ) + ) let mycnfPath = expandHome(config, MyCnf) if safeFileExists(mycnfPath): let content = readFileContent(mycnfPath) let hasPassword = "password" in content.toLowerAscii() - let sev = if isWorldReadable(mycnfPath): svCritical - elif hasPassword: svHigh - else: svMedium - - result.findings.add(makeFinding( - mycnfPath, - "MySQL configuration" & (if hasPassword: " (contains password)" else: ""), - catApptoken, sev - )) + let sev = + if isWorldReadable(mycnfPath): + svCritical + elif hasPassword: + svHigh + else: + svMedium + + result.findings.add( + makeFinding( + mycnfPath, + "MySQL configuration" & (if hasPassword: " (contains password)" else: ""), + catApptoken, + sev, + ) + ) let redisPath = expandHome(config, RedisConf) if safeFileExists(redisPath): let sev = if isWorldReadable(redisPath): svCritical else: svHigh - result.findings.add(makeFinding( - redisPath, - "Redis CLI authentication file", - catApptoken, sev - )) + result.findings.add( + makeFinding(redisPath, "Redis CLI authentication file", catApptoken, sev) + ) let mongoPath = expandHome(config, MongoRc) if safeFileExists(mongoPath): let content = readFileContent(mongoPath) - let hasCreds = "password" in content.toLowerAscii() or - "auth" in content.toLowerAscii() - let sev = if isWorldReadable(mongoPath): svCritical - elif hasCreds: svHigh - else: svMedium - - result.findings.add(makeFinding( - mongoPath, - "MongoDB RC file" & (if hasCreds: " (may contain credentials)" else: ""), - catApptoken, sev - )) + let hasCreds = + "password" in content.toLowerAscii() or "auth" in content.toLowerAscii() + let sev = + if isWorldReadable(mongoPath): + svCritical + elif hasCreds: + svHigh + else: + svMedium + + result.findings.add( + makeFinding( + mongoPath, + "MongoDB RC file" & (if hasCreds: " (may contain credentials)" else: ""), + catApptoken, + sev, + ) + ) proc scanNetrc(config: HarvestConfig, result: var CollectorResult) = let path = expandHome(config, NetrcFile) @@ -122,125 +156,151 @@ proc scanNetrc(config: HarvestConfig, result: var CollectorResult) = if "password " in stripped.toLowerAscii(): hasPassword = true - let sev = if isWorldReadable(path): svCritical - elif hasPassword: svHigh - else: svMedium + let sev = + if isWorldReadable(path): + svCritical + elif hasPassword: + svHigh + else: + svMedium var cred = Credential( source: path, credType: "netrc_credentials", preview: $machineCount & " machine entries", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("machines", $machineCount) cred.setMeta("has_password", $hasPassword) - result.findings.add(makeFindingWithCred( - path, - "Netrc credential file with " & $machineCount & " entries", - catApptoken, sev, cred - )) + result.findings.add( + makeFindingWithCred( + path, + "Netrc credential file with " & $machineCount & " entries", + catApptoken, + sev, + cred, + ) + ) proc scanDevTokenFiles(config: HarvestConfig, result: var CollectorResult) = let npmrcPath = expandHome(config, NpmrcFile) if safeFileExists(npmrcPath): let content = readFileContent(npmrcPath) let hasToken = "_authToken" in content or "_auth" in content - let sev = if isWorldReadable(npmrcPath): svCritical - elif hasToken: svHigh - else: svInfo + let sev = + if isWorldReadable(npmrcPath): + svCritical + elif hasToken: + svHigh + else: + svInfo if hasToken: - result.findings.add(makeFinding( - npmrcPath, - "npm registry authentication token", - catApptoken, sev - )) + result.findings.add( + makeFinding(npmrcPath, "npm registry authentication token", catApptoken, sev) + ) let pypircPath = expandHome(config, PypircFile) if safeFileExists(pypircPath): let content = readFileContent(pypircPath) let hasPassword = "password" in content.toLowerAscii() - let sev = if isWorldReadable(pypircPath): svCritical - elif hasPassword: svHigh - else: svMedium - - result.findings.add(makeFinding( - pypircPath, - "PyPI configuration" & (if hasPassword: " (contains credentials)" else: ""), - catApptoken, sev - )) + let sev = + if isWorldReadable(pypircPath): + svCritical + elif hasPassword: + svHigh + else: + svMedium + + result.findings.add( + makeFinding( + pypircPath, + "PyPI configuration" & (if hasPassword: " (contains credentials)" else: ""), + catApptoken, + sev, + ) + ) let ghPath = expandHome(config, GhCliHosts) if safeFileExists(ghPath): let content = readFileContent(ghPath) let hasOauth = "oauth_token" in content.toLowerAscii() - let sev = if isWorldReadable(ghPath): svCritical - elif hasOauth: svHigh - else: svMedium + let sev = + if isWorldReadable(ghPath): + svCritical + elif hasOauth: + svHigh + else: + svMedium - result.findings.add(makeFinding( - ghPath, - "GitHub CLI OAuth token", - catApptoken, sev - )) + result.findings.add(makeFinding(ghPath, "GitHub CLI OAuth token", catApptoken, sev)) proc scanInfraTokenFiles(config: HarvestConfig, result: var CollectorResult) = let tfPath = expandHome(config, TerraformCreds) if safeFileExists(tfPath): let content = readFileContent(tfPath) let hasToken = "token" in content.toLowerAscii() - let sev = if isWorldReadable(tfPath): svCritical - elif hasToken: svHigh - else: svMedium - - result.findings.add(makeFinding( - tfPath, - "Terraform Cloud API token", - catApptoken, sev - )) + let sev = + if isWorldReadable(tfPath): + svCritical + elif hasToken: + svHigh + else: + svMedium + + result.findings.add( + makeFinding(tfPath, "Terraform Cloud API token", catApptoken, sev) + ) let vaultPath = expandHome(config, VaultTokenFile) if safeFileExists(vaultPath): let sev = if isWorldReadable(vaultPath): svCritical else: svHigh - result.findings.add(makeFinding( - vaultPath, - "HashiCorp Vault token", - catApptoken, sev - )) + result.findings.add( + makeFinding(vaultPath, "HashiCorp Vault token", catApptoken, sev) + ) let helmPath = expandHome(config, HelmRepos) if safeFileExists(helmPath): let content = readFileContent(helmPath) let hasPassword = "password" in content.toLowerAscii() - let sev = if isWorldReadable(helmPath): svCritical - elif hasPassword: svHigh - else: svInfo + let sev = + if isWorldReadable(helmPath): + svCritical + elif hasPassword: + svHigh + else: + svInfo if hasPassword: - result.findings.add(makeFinding( - helmPath, - "Helm repository credentials", - catApptoken, sev - )) + result.findings.add( + makeFinding(helmPath, "Helm repository credentials", catApptoken, sev) + ) let rclonePath = expandHome(config, RcloneConf) if safeFileExists(rclonePath): let content = readFileContent(rclonePath) - let hasCreds = "pass" in content.toLowerAscii() or - "token" in content.toLowerAscii() or - "key" in content.toLowerAscii() - let sev = if isWorldReadable(rclonePath): svCritical - elif hasCreds: svHigh - else: svMedium - - result.findings.add(makeFinding( - rclonePath, - "Rclone cloud storage configuration" & - (if hasCreds: " (contains credentials)" else: ""), - catApptoken, sev - )) + let hasCreds = + "pass" in content.toLowerAscii() or "token" in content.toLowerAscii() or + "key" in content.toLowerAscii() + let sev = + if isWorldReadable(rclonePath): + svCritical + elif hasCreds: + svHigh + else: + svMedium + + result.findings.add( + makeFinding( + rclonePath, + "Rclone cloud storage configuration" & + (if hasCreds: " (contains credentials)" else: ""), + catApptoken, + sev, + ) + ) proc scanDockerConfig(config: HarvestConfig, result: var CollectorResult) = let dockerPath = expandHome(config, DockerConfig) @@ -249,36 +309,60 @@ proc scanDockerConfig(config: HarvestConfig, result: var CollectorResult) = let content = readFileContent(dockerPath) let hasAuth = "\"auth\"" in content or "\"auths\"" in content - let sev = if isWorldReadable(dockerPath): svCritical - elif hasAuth: svHigh - else: svMedium + let sev = + if isWorldReadable(dockerPath): + svCritical + elif hasAuth: + svHigh + else: + svMedium var cred = Credential( source: dockerPath, credType: "docker_registry_auth", preview: if hasAuth: "Registry authentication tokens present" else: "No auth data", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) - result.findings.add(makeFindingWithCred( - dockerPath, - "Docker configuration" & (if hasAuth: " with registry auth tokens" else: ""), - catApptoken, sev, cred - )) + result.findings.add( + makeFindingWithCred( + dockerPath, + "Docker configuration" & (if hasAuth: " with registry auth tokens" else: ""), + catApptoken, + sev, + cred, + ) + ) proc collect*(config: HarvestConfig): CollectorResult = result = newCollectorResult("apptoken", catApptoken) let start = getMonoTime() let appTargets = [ - AppTarget(path: SlackDir, name: "Slack", - description: "Slack desktop session data", isDir: true), - AppTarget(path: DiscordDir, name: "Discord", - description: "Discord desktop session data", isDir: true), - AppTarget(path: VsCodeDir, name: "VS Code", - description: "VS Code configuration directory", isDir: true), - AppTarget(path: VsCodeUserSettings, name: "VS Code Settings", - description: "VS Code user settings (may contain tokens)", isDir: false) + AppTarget( + path: SlackDir, + name: "Slack", + description: "Slack desktop session data", + isDir: true, + ), + AppTarget( + path: DiscordDir, + name: "Discord", + description: "Discord desktop session data", + isDir: true, + ), + AppTarget( + path: VsCodeDir, + name: "VS Code", + description: "VS Code configuration directory", + isDir: true, + ), + AppTarget( + path: VsCodeUserSettings, + name: "VS Code Settings", + description: "VS Code user settings (may contain tokens)", + isDir: false, + ), ] for target in appTargets: diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/base.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/base.nim index f6e3f995..bd461524 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/base.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/base.nim @@ -1,5 +1,24 @@ # ©AngelaMos | 2026 # base.nim +# +# Shared collector utilities and Finding constructors +# +# Foundation layer used by all seven collector modules. Wraps POSIX +# stat for permission inspection (getPermsString, getNumericPerms, +# isWorldReadable, isGroupReadable), provides safe filesystem access +# (safeFileExists, safeDirExists, readFileContent, readFileLines with +# optional line cap), SYSROOT-aware path expansion via expandHome, +# and exclude-pattern matching. Constructs Finding objects through +# makeFinding and makeFindingWithCred (auto-populates permissions, +# modification time, and file size). permissionSeverity maps file +# modes to severity levels based on world/group read bits. Also +# provides redactValue for credential preview masking and setMeta +# for type-safe metadata insertion. +# +# Connects to: +# types.nim - Finding, Credential, Severity, Category, HarvestConfig +# config.nim - OwnerOnlyFilePerms, OwnerOnlyDirPerms, WorldReadBit, +# GroupReadBit {.push raises: [].} @@ -94,10 +113,7 @@ proc matchesExclude*(path: string, patterns: seq[string]): bool = return true proc makeFinding*( - path: string, - description: string, - category: Category, - severity: Severity + path: string, description: string, category: Category, severity: Severity ): Finding = Finding( path: path, @@ -107,15 +123,15 @@ proc makeFinding*( credential: none(Credential), permissions: getPermsString(path), modified: getModifiedTime(path), - size: getFileSizeBytes(path) + size: getFileSizeBytes(path), ) proc makeFindingWithCred*( - path: string, - description: string, - category: Category, - severity: Severity, - cred: Credential + path: string, + description: string, + category: Category, + severity: Severity, + cred: Credential, ): Finding = Finding( path: path, @@ -125,16 +141,12 @@ proc makeFindingWithCred*( credential: some(cred), permissions: getPermsString(path), modified: getModifiedTime(path), - size: getFileSizeBytes(path) + size: getFileSizeBytes(path), ) proc newCollectorResult*(name: string, category: Category): CollectorResult = CollectorResult( - name: name, - category: category, - findings: @[], - durationMs: 0, - errors: @[] + name: name, category: category, findings: @[], durationMs: 0, errors: @[] ) proc permissionSeverity*(path: string, isDir: bool = false): Severity = diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/browser.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/browser.nim index 0efe472b..7eb3a514 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/browser.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/browser.nim @@ -1,5 +1,24 @@ # ©AngelaMos | 2026 # browser.nim +# +# Browser credential store collector +# +# Detects Firefox and Chromium-family browser credential databases. +# scanFirefox parses profiles.ini to discover profile directories, +# then checks each for logins.json (stored passwords), cookies.sqlite, +# and key4.db (master key database). scanChromium iterates four +# browser paths (Chrome, Chromium, Brave, Vivaldi) and their Default/ +# Profile N subdirectories looking for Login Data, Cookies, and Web +# Data (autofill and payment methods) SQLite databases. Severity +# escalates based on file permissions (world-readable = critical, +# group-readable = high, owner-only = medium). +# +# Connects to: +# collectors/base.nim - expandHome, safeFileExists, safeDirExists, +# isWorldReadable, isGroupReadable, makeFinding +# config.nim - FirefoxDir, FirefoxProfilesIni, FirefoxLoginsFile, +# FirefoxCookiesDb, FirefoxKeyDb, ChromiumDirs, +# ChromiumLoginData, ChromiumCookies, ChromiumWebData {.push raises: [].} @@ -35,8 +54,11 @@ proc scanFirefox(config: HarvestConfig, result: var CollectorResult) = profiles.add(currentPath) for profile in profiles: - let profileDir = if profile.startsWith("/"): profile - else: firefoxPath / profile + let profileDir = + if profile.startsWith("/"): + profile + else: + firefoxPath / profile if not safeDirExists(profileDir): continue @@ -44,15 +66,19 @@ proc scanFirefox(config: HarvestConfig, result: var CollectorResult) = let credFiles = [ (FirefoxLoginsFile, "Firefox stored logins database"), (FirefoxCookiesDb, "Firefox cookies database"), - (FirefoxKeyDb, "Firefox key database") + (FirefoxKeyDb, "Firefox key database"), ] for (fileName, desc) in credFiles: let filePath = profileDir / fileName if safeFileExists(filePath): - let sev = if isWorldReadable(filePath): svCritical - elif isGroupReadable(filePath): svHigh - else: svMedium + let sev = + if isWorldReadable(filePath): + svCritical + elif isGroupReadable(filePath): + svHigh + else: + svMedium result.findings.add(makeFinding(filePath, desc, catBrowser, sev)) @@ -81,15 +107,19 @@ proc scanChromium(config: HarvestConfig, result: var CollectorResult) = let credFiles = [ (ChromiumLoginData, browserName & " stored login database"), (ChromiumCookies, browserName & " cookies database"), - (ChromiumWebData, browserName & " web data (autofill, payment methods)") + (ChromiumWebData, browserName & " web data (autofill, payment methods)"), ] for (fileName, desc) in credFiles: let filePath = profileDir / fileName if safeFileExists(filePath): - let sev = if isWorldReadable(filePath): svCritical - elif isGroupReadable(filePath): svHigh - else: svMedium + let sev = + if isWorldReadable(filePath): + svCritical + elif isGroupReadable(filePath): + svHigh + else: + svMedium result.findings.add(makeFinding(filePath, desc, catBrowser, sev)) diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/cloud.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/cloud.nim index 13efdd75..601892d6 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/cloud.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/cloud.nim @@ -1,5 +1,27 @@ # ©AngelaMos | 2026 # cloud.nim +# +# Cloud provider configuration collector +# +# Scans for credential exposure across four cloud platforms. scanAws +# parses ~/.aws/credentials for profile counts, static keys (AKIA +# prefix), and session keys (ASIA prefix), then checks ~/.aws/config +# for SSO and MFA configuration. scanGcp inspects application default +# credentials for service account vs user type and walks the gcloud +# config directory for additional service account key files. scanAzure +# checks for access token and MSAL token cache files. scanKubernetes +# parses ~/.kube/config to count contexts and users, detecting token- +# based and certificate-based authentication methods. Severity +# escalates for static keys, service accounts, token auth, and +# world-readable files. +# +# Connects to: +# collectors/base.nim - expandHome, safeFileExists, safeDirExists, +# readFileContent, readFileLines, isWorldReadable, +# makeFinding, makeFindingWithCred, permissionSeverity +# config.nim - AwsCredentials, AwsConfig, AwsStaticKeyPrefix, +# GcpConfigDir, GcpAppDefaultCreds, AzureDir, +# AzureAccessTokens, KubeConfig, KubeContextMarker {.push raises: [].} @@ -42,18 +64,22 @@ proc scanAws(config: HarvestConfig, result: var CollectorResult) = source: credPath, credType: "aws_credentials", preview: $profileCount & " profiles, " & $staticKeys & " static keys", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("profiles", $profileCount) cred.setMeta("static_keys", $staticKeys) cred.setMeta("session_keys", $sessionKeys) - result.findings.add(makeFindingWithCred( - credPath, - "AWS credentials file: " & $profileCount & " profiles, " & - $staticKeys & " static keys, " & $sessionKeys & " session keys", - catCloud, sev, cred - )) + result.findings.add( + makeFindingWithCred( + credPath, + "AWS credentials file: " & $profileCount & " profiles, " & $staticKeys & + " static keys, " & $sessionKeys & " session keys", + catCloud, + sev, + cred, + ) + ) if safeFileExists(configPath): let lines = readFileLines(configPath) @@ -91,16 +117,20 @@ proc scanGcp(config: HarvestConfig, result: var CollectorResult) = source: adcPath, credType: "gcp_credentials", preview: if isServiceAccount: "Service account key" else: "User credentials", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) let credTypeStr = if isServiceAccount: "service_account" else: "authorized_user" cred.setMeta("type", credTypeStr) - result.findings.add(makeFindingWithCred( - adcPath, - "GCP application default credentials (" & credTypeStr & ")", - catCloud, sev, cred - )) + result.findings.add( + makeFindingWithCred( + adcPath, + "GCP application default credentials (" & credTypeStr & ")", + catCloud, + sev, + cred, + ) + ) if safeDirExists(gcpDir): try: @@ -110,11 +140,9 @@ proc scanGcp(config: HarvestConfig, result: var CollectorResult) = if path.endsWith(".json") and path != adcPath: let content = readFileContent(path) if GcpServiceAccountPattern in content.toLowerAscii(): - result.findings.add(makeFinding( - path, - "GCP service account key file", - catCloud, svHigh - )) + result.findings.add( + makeFinding(path, "GCP service account key file", catCloud, svHigh) + ) except CatchableError as e: result.errors.add("Error scanning GCP directory: " & e.msg) @@ -123,28 +151,20 @@ proc scanAzure(config: HarvestConfig, result: var CollectorResult) = if not safeDirExists(azDir): return - let tokenPaths = [ - expandHome(config, AzureAccessTokens), - expandHome(config, AzureMsalTokenCache) - ] + let tokenPaths = + [expandHome(config, AzureAccessTokens), expandHome(config, AzureMsalTokenCache)] var foundTokens = false for path in tokenPaths: if safeFileExists(path): foundTokens = true let sev = if isWorldReadable(path): svCritical else: svMedium - result.findings.add(makeFinding( - path, - "Azure token cache", - catCloud, sev - )) + result.findings.add(makeFinding(path, "Azure token cache", catCloud, sev)) if not foundTokens: - result.findings.add(makeFinding( - azDir, - "Azure CLI configuration directory", - catCloud, svInfo - )) + result.findings.add( + makeFinding(azDir, "Azure CLI configuration directory", catCloud, svInfo) + ) proc scanKubernetes(config: HarvestConfig, result: var CollectorResult) = let kubePath = expandHome(config, KubeConfig) @@ -170,7 +190,7 @@ proc scanKubernetes(config: HarvestConfig, result: var CollectorResult) = inUsers = true inContexts = false elif stripped.len > 0 and not stripped.startsWith(" ") and - not stripped.startsWith("-"): + not stripped.startsWith("-"): inContexts = false inUsers = false @@ -183,26 +203,34 @@ proc scanKubernetes(config: HarvestConfig, result: var CollectorResult) = if "client-certificate-data:" in stripped: hasCertAuth = true - let sev = if isWorldReadable(kubePath): svCritical - elif hasTokenAuth: svHigh - else: svMedium + let sev = + if isWorldReadable(kubePath): + svCritical + elif hasTokenAuth: + svHigh + else: + svMedium var cred = Credential( source: kubePath, credType: "kubernetes_config", preview: $contextCount & " contexts, " & $userCount & " users", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("contexts", $contextCount) cred.setMeta("users", $userCount) cred.setMeta("token_auth", $hasTokenAuth) cred.setMeta("cert_auth", $hasCertAuth) - result.findings.add(makeFindingWithCred( - kubePath, - "Kubernetes config: " & $contextCount & " contexts, " & $userCount & " users", - catCloud, sev, cred - )) + result.findings.add( + makeFindingWithCred( + kubePath, + "Kubernetes config: " & $contextCount & " contexts, " & $userCount & " users", + catCloud, + sev, + cred, + ) + ) proc collect*(config: HarvestConfig): CollectorResult = result = newCollectorResult("cloud", catCloud) diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/git.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/git.nim index 12e9d7e4..f67be6e1 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/git.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/git.nim @@ -1,5 +1,25 @@ # ©AngelaMos | 2026 # git.nim +# +# Git credential store and token collector +# +# Scans for Git-related credential exposure in three areas. +# scanGitCredentials reads ~/.git-credentials for plaintext URL +# entries containing embedded passwords (severity high, critical if +# world-readable). scanGitConfig parses ~/.gitconfig and +# ~/.config/git/config for credential helper configuration, flagging +# the "store" helper as medium severity since it persists plaintext. +# scanTokenPatterns searches Git config files for GitHub personal +# access token prefixes (ghp_, gho_, ghu_, ghs_, ghr_) and GitLab +# token prefixes (glpat-), reporting detected tokens with redacted +# previews. +# +# Connects to: +# collectors/base.nim - expandHome, safeFileExists, readFileContent, +# readFileLines, isWorldReadable, getPermsString, +# makeFinding, makeFindingWithCred, redactValue +# config.nim - GitCredentials, GitConfig, GitConfigLocal, +# GitHubTokenPatterns, GitLabTokenPatterns {.push raises: [].} @@ -28,24 +48,25 @@ proc scanGitCredentials(config: HarvestConfig, result: var CollectorResult) = source: credPath, credType: "git_plaintext_credentials", preview: $credCount & " stored credentials", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("count", $credCount) cred.setMeta("permissions", getPermsString(credPath)) let sev = if isWorldReadable(credPath): svCritical else: svHigh - result.findings.add(makeFindingWithCred( - credPath, - "Plaintext Git credential store with " & $credCount & " entries", - catGit, sev, cred - )) + result.findings.add( + makeFindingWithCred( + credPath, + "Plaintext Git credential store with " & $credCount & " entries", + catGit, + sev, + cred, + ) + ) proc scanGitConfig(config: HarvestConfig, result: var CollectorResult) = - let paths = [ - expandHome(config, GitConfig), - expandHome(config, GitConfigLocal) - ] + let paths = [expandHome(config, GitConfig), expandHome(config, GitConfigLocal)] for path in paths: if not safeFileExists(path): @@ -71,17 +92,14 @@ proc scanGitConfig(config: HarvestConfig, result: var CollectorResult) = if helperValue.len > 0: let sev = if helperValue == "store": svMedium else: svInfo - result.findings.add(makeFinding( - path, - "Git credential helper configured: " & helperValue, - catGit, sev - )) + result.findings.add( + makeFinding( + path, "Git credential helper configured: " & helperValue, catGit, sev + ) + ) proc scanTokenPatterns(config: HarvestConfig, result: var CollectorResult) = - let configPaths = [ - expandHome(config, GitConfig), - expandHome(config, GitConfigLocal) - ] + let configPaths = [expandHome(config, GitConfig), expandHome(config, GitConfigLocal)] for path in configPaths: if not safeFileExists(path): @@ -99,13 +117,13 @@ proc scanTokenPatterns(config: HarvestConfig, result: var CollectorResult) = source: path, credType: "github_token", preview: redactValue(tokenStart, 8), - metadata: initTable[string, string]() + metadata: initTable[string, string](), + ) + result.findings.add( + makeFindingWithCred( + path, "GitHub personal access token detected", catGit, svHigh, cred + ) ) - result.findings.add(makeFindingWithCred( - path, - "GitHub personal access token detected", - catGit, svHigh, cred - )) break for pattern in GitLabTokenPatterns: @@ -116,13 +134,13 @@ proc scanTokenPatterns(config: HarvestConfig, result: var CollectorResult) = source: path, credType: "gitlab_token", preview: redactValue(tokenStart, 8), - metadata: initTable[string, string]() + metadata: initTable[string, string](), + ) + result.findings.add( + makeFindingWithCred( + path, "GitLab personal access token detected", catGit, svHigh, cred + ) ) - result.findings.add(makeFindingWithCred( - path, - "GitLab personal access token detected", - catGit, svHigh, cred - )) break proc collect*(config: HarvestConfig): CollectorResult = diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/history.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/history.nim index d6eacfdf..8ec7ad13 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/history.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/history.nim @@ -1,5 +1,27 @@ # ©AngelaMos | 2026 # history.nim +# +# Shell history and environment file collector +# +# Scans for secrets leaked through shell history and .env files. +# scanHistoryFile reads up to 50000 lines from each history file +# (.bash_history, .zsh_history, .fish_history, .sh_history, +# .python_history) and matches lines against two pattern sets: +# secret assignments (KEY=, SECRET=, TOKEN=, PASSWORD=, etc. with +# export prefix detection) capped at 20 reported findings, and +# sensitive commands (curl with auth headers, wget with passwords, +# mysql -p, psql password, sshpass) capped at 10. Redacts matched +# values using redactLine to avoid exposing actual secrets. +# scanEnvFiles recursively walks subdirectories up to depth 5 looking +# for .env, .env.local, .env.production, and .env.staging files, +# skipping hidden dirs and common vendored paths. +# +# Connects to: +# collectors/base.nim - expandHome, safeFileExists, readFileLines, +# isWorldReadable, isGroupReadable, makeFinding, +# makeFindingWithCred, matchesExclude, redactValue +# config.nim - HistoryFiles, SecretPatterns, +# HistoryCommandPatterns, EnvFilePatterns {.push raises: [].} @@ -21,11 +43,12 @@ proc redactLine*(line: string): string = if valStart >= line.len: return line let value = line[valStart .. ^1].strip() - let cleanValue = if (value.startsWith("\"") and value.endsWith("\"")) or - (value.startsWith("'") and value.endsWith("'")): - value[1 ..< ^1] - else: - value + let cleanValue = + if (value.startsWith("\"") and value.endsWith("\"")) or + (value.startsWith("'") and value.endsWith("'")): + value[1 ..< ^1] + else: + value result = key & "=" & redactValue(cleanValue, 4) proc matchesSecretPattern*(line: string): bool = @@ -33,7 +56,7 @@ proc matchesSecretPattern*(line: string): bool = for pattern in SecretPatterns: if pattern in upper: if "export " in line.toLowerAscii() or - line.strip().startsWith(pattern.split("=")[0]): + line.strip().startsWith(pattern.split("=")[0]): return true proc matchesCommandPattern*(line: string): bool = @@ -55,9 +78,7 @@ proc matchesCommandPattern*(line: string): bool = return true proc scanHistoryFile( - config: HarvestConfig, - fileName: string, - result: var CollectorResult + config: HarvestConfig, fileName: string, result: var CollectorResult ) = let path = expandHome(config, fileName) if not safeFileExists(path): @@ -79,40 +100,46 @@ proc scanHistoryFile( source: path, credType: "history_secret", preview: redactLine(stripped), - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("line_region", $(i + 1)) - result.findings.add(makeFindingWithCred( - path, - "Secret in shell history (line ~" & $(i + 1) & ")", - catHistory, svHigh, cred - )) - + result.findings.add( + makeFindingWithCred( + path, + "Secret in shell history (line ~" & $(i + 1) & ")", + catHistory, + svHigh, + cred, + ) + ) elif matchesCommandPattern(stripped): inc commandCount if commandCount <= 10: - let preview = if stripped.len > 60: stripped[0 ..< 60] & "..." - else: stripped - - result.findings.add(makeFinding( - path, - "Sensitive command in history: " & preview, - catHistory, svMedium - )) + let preview = + if stripped.len > 60: + stripped[0 ..< 60] & "..." + else: + stripped + + result.findings.add( + makeFinding( + path, "Sensitive command in history: " & preview, catHistory, svMedium + ) + ) if secretCount > 20: - result.findings.add(makeFinding( - path, - $secretCount & " total secret patterns found (showing first 20)", - catHistory, svInfo - )) + result.findings.add( + makeFinding( + path, + $secretCount & " total secret patterns found (showing first 20)", + catHistory, + svInfo, + ) + ) proc walkForEnv( - dir: string, - depth: int, - excludePatterns: seq[string], - result: var CollectorResult + dir: string, depth: int, excludePatterns: seq[string], result: var CollectorResult ) = if depth > MaxEnvDepth: return @@ -125,21 +152,23 @@ proc walkForEnv( let name = path.extractFilename() for envPattern in EnvFilePatterns: if name == envPattern: - let sev = if isWorldReadable(path): svCritical - elif isGroupReadable(path): svHigh - else: svMedium - result.findings.add(makeFinding( - path, - "Environment file: " & name, - catHistory, sev - )) + let sev = + if isWorldReadable(path): + svCritical + elif isGroupReadable(path): + svHigh + else: + svMedium + result.findings.add( + makeFinding(path, "Environment file: " & name, catHistory, sev) + ) break of pcDir: let dirName = path.extractFilename() if dirName.startsWith(".") and dirName notin [".config", ".local"]: continue - if dirName in ["node_modules", "vendor", ".git", "__pycache__", - ".venv", "venv", ".cache"]: + if dirName in + ["node_modules", "vendor", ".git", "__pycache__", ".venv", "venv", ".cache"]: continue walkForEnv(path, depth + 1, excludePatterns, result) else: diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/keyring.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/keyring.nim index 47d89bce..9c719f09 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/keyring.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/keyring.nim @@ -1,5 +1,25 @@ # ©AngelaMos | 2026 # keyring.nim +# +# Desktop keyring and password manager collector +# +# Detects credential stores from five sources. scanGnomeKeyring walks +# ~/.local/share/keyrings for .keyring database files. scanKdeWallet +# checks ~/.local/share/kwalletd for wallet files. scanKeePass +# recursively searches up to depth 5 for .kdbx database files, +# skipping hidden/vendored directories. scanPassStore checks +# ~/.password-store and counts GPG-encrypted entries. scanBitwarden +# checks for Bitwarden desktop and CLI local vault directories. +# Each finding's severity is based on file permissions (world-readable +# escalates to critical). +# +# Connects to: +# collectors/base.nim - expandHome, safeDirExists, safeFileExists, +# isWorldReadable, isGroupReadable, makeFinding, +# makeFindingWithCred, matchesExclude, +# permissionSeverity +# config.nim - GnomeKeyringDir, KdeWalletDir, KeePassExtension, +# PassStoreDir, BitwardenDir, BitwardenCliDir {.push raises: [].} @@ -20,22 +40,24 @@ proc scanGnomeKeyring(config: HarvestConfig, result: var CollectorResult) = continue if path.endsWith(".keyring"): inc dbCount - let sev = if isWorldReadable(path): svCritical - elif isGroupReadable(path): svHigh - else: svMedium - - result.findings.add(makeFinding( - path, - "GNOME Keyring database", - catKeyring, sev - )) + let sev = + if isWorldReadable(path): + svCritical + elif isGroupReadable(path): + svHigh + else: + svMedium + + result.findings.add( + makeFinding(path, "GNOME Keyring database", catKeyring, sev) + ) if dbCount == 0: - result.findings.add(makeFinding( - keyringDir, - "GNOME Keyring directory exists (empty)", - catKeyring, svInfo - )) + result.findings.add( + makeFinding( + keyringDir, "GNOME Keyring directory exists (empty)", catKeyring, svInfo + ) + ) except CatchableError as e: result.errors.add("Error scanning GNOME Keyring: " & e.msg) @@ -48,23 +70,20 @@ proc scanKdeWallet(config: HarvestConfig, result: var CollectorResult) = for kind, path in walkDir(walletDir): if kind != pcFile: continue - let sev = if isWorldReadable(path): svCritical - elif isGroupReadable(path): svHigh - else: svMedium - - result.findings.add(makeFinding( - path, - "KDE Wallet database", - catKeyring, sev - )) + let sev = + if isWorldReadable(path): + svCritical + elif isGroupReadable(path): + svHigh + else: + svMedium + + result.findings.add(makeFinding(path, "KDE Wallet database", catKeyring, sev)) except CatchableError as e: result.errors.add("Error scanning KDE Wallet: " & e.msg) proc walkForKdbx( - dir: string, - depth: int, - excludePatterns: seq[string], - result: var CollectorResult + dir: string, depth: int, excludePatterns: seq[string], result: var CollectorResult ) = if depth > 5: return @@ -75,22 +94,24 @@ proc walkForKdbx( case kind of pcFile: if path.endsWith(KeePassExtension): - let sev = if isWorldReadable(path): svCritical - elif isGroupReadable(path): svHigh - else: svMedium - - result.findings.add(makeFinding( - path, - "KeePass database file", - catKeyring, sev - )) + let sev = + if isWorldReadable(path): + svCritical + elif isGroupReadable(path): + svHigh + else: + svMedium + + result.findings.add( + makeFinding(path, "KeePass database file", catKeyring, sev) + ) of pcDir: let dirName = path.extractFilename() if dirName.startsWith(".") and - dirName notin [".config", ".local", ".keepass", ".keepassxc"]: + dirName notin [".config", ".local", ".keepass", ".keepassxc"]: continue - if dirName in ["node_modules", "vendor", ".git", "__pycache__", - ".venv", "venv", ".cache"]: + if dirName in + ["node_modules", "vendor", ".git", "__pycache__", ".venv", "venv", ".cache"]: continue walkForKdbx(path, depth + 1, excludePatterns, result) else: @@ -118,30 +139,29 @@ proc scanPassStore(config: HarvestConfig, result: var CollectorResult) = source: passDir, credType: "pass_store", preview: $entryCount & " encrypted entries", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("entry_count", $entryCount) - result.findings.add(makeFindingWithCred( - passDir, - "pass (password-store) with " & $entryCount & " entries", - catKeyring, svInfo, cred - )) + result.findings.add( + makeFindingWithCred( + passDir, + "pass (password-store) with " & $entryCount & " entries", + catKeyring, + svInfo, + cred, + ) + ) proc scanBitwarden(config: HarvestConfig, result: var CollectorResult) = - let dirs = [ - expandHome(config, BitwardenDir), - expandHome(config, BitwardenCliDir) - ] + let dirs = [expandHome(config, BitwardenDir), expandHome(config, BitwardenCliDir)] for dir in dirs: if safeDirExists(dir): let sev = permissionSeverity(dir, isDir = true) - result.findings.add(makeFinding( - dir, - "Bitwarden local vault data", - catKeyring, sev - )) + result.findings.add( + makeFinding(dir, "Bitwarden local vault data", catKeyring, sev) + ) proc collect*(config: HarvestConfig): CollectorResult = result = newCollectorResult("keyring", catKeyring) diff --git a/PROJECTS/intermediate/credential-enumeration/src/collectors/ssh.nim b/PROJECTS/intermediate/credential-enumeration/src/collectors/ssh.nim index 1fbb3979..c1fe03f3 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/collectors/ssh.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/collectors/ssh.nim @@ -1,5 +1,26 @@ # ©AngelaMos | 2026 # ssh.nim +# +# SSH key and configuration collector +# +# Scans the target's ~/.ssh directory for credential exposure across +# four areas. scanKeys walks all files looking for PEM/OpenSSH private +# key headers, classifies each as encrypted or unencrypted by checking +# for passphrase markers (ENCRYPTED, bcrypt, aes256-ctr), and escalates +# severity based on encryption status and file permissions (world/group +# readable). scanConfig parses ssh_config for host entry counts and +# weak settings (PasswordAuthentication yes, StrictHostKeyChecking no). +# scanAuthorizedKeys counts non-comment public key entries. +# scanKnownHosts counts known host entries. Also validates .ssh +# directory permissions against the expected 0700. +# +# Connects to: +# collectors/base.nim - expandHome, safeFileExists, safeDirExists, +# readFileContent, readFileLines, makeFinding, +# makeFindingWithCred, permissionSeverity +# config.nim - SshDir, SshKeyHeaders, SshEncryptedMarkers, +# SshSafeDirPerms, SshConfig, SshAuthorizedKeys, +# SshKnownHosts {.push raises: [].} @@ -26,12 +47,15 @@ proc scanKeys(config: HarvestConfig, result: var CollectorResult) = let dirPerms = getNumericPerms(sshPath) if dirPerms >= 0 and dirPerms != OwnerOnlyDirPerms: let sev = permissionSeverity(sshPath, isDir = true) - result.findings.add(makeFinding( - sshPath, - "SSH directory permissions " & getPermsString(sshPath) & - " (expected " & SshSafeDirPerms & ")", - catSsh, sev - )) + result.findings.add( + makeFinding( + sshPath, + "SSH directory permissions " & getPermsString(sshPath) & " (expected " & + SshSafeDirPerms & ")", + catSsh, + sev, + ) + ) try: for kind, path in walkDir(sshPath): @@ -62,22 +86,29 @@ proc scanKeys(config: HarvestConfig, result: var CollectorResult) = if sev < svHigh: sev = svHigh - let keyType = if content.startsWith(SshKeyHeaders[0]): "OpenSSH" - elif content.startsWith(SshKeyHeaders[1]): "RSA" - elif content.startsWith(SshKeyHeaders[2]): "ECDSA" - elif content.startsWith(SshKeyHeaders[3]): "DSA" - else: "Unknown" - - let desc = if encrypted: - keyType & " private key (passphrase-protected)" - else: - keyType & " private key (no passphrase)" + let keyType = + if content.startsWith(SshKeyHeaders[0]): + "OpenSSH" + elif content.startsWith(SshKeyHeaders[1]): + "RSA" + elif content.startsWith(SshKeyHeaders[2]): + "ECDSA" + elif content.startsWith(SshKeyHeaders[3]): + "DSA" + else: + "Unknown" + + let desc = + if encrypted: + keyType & " private key (passphrase-protected)" + else: + keyType & " private key (no passphrase)" var cred = Credential( source: path, credType: "ssh_private_key", preview: keyType & " key", - metadata: initTable[string, string]() + metadata: initTable[string, string](), ) cred.setMeta("encrypted", $encrypted) cred.setMeta("permissions", getPermsString(path)) @@ -98,7 +129,7 @@ proc scanConfig(config: HarvestConfig, result: var CollectorResult) = for line in lines: let stripped = line.strip() if stripped.toLowerAscii().startsWith("host ") and - not stripped.toLowerAscii().startsWith("host *"): + not stripped.toLowerAscii().startsWith("host *"): inc hostCount if stripped.toLowerAscii().startsWith("passwordauthentication yes"): @@ -108,18 +139,16 @@ proc scanConfig(config: HarvestConfig, result: var CollectorResult) = weakSettings.add("StrictHostKeyChecking disabled") if hostCount > 0: - result.findings.add(makeFinding( - configPath, - "SSH config with " & $hostCount & " host entries", - catSsh, svInfo - )) + result.findings.add( + makeFinding( + configPath, "SSH config with " & $hostCount & " host entries", catSsh, svInfo + ) + ) for setting in weakSettings: - result.findings.add(makeFinding( - configPath, - "Weak SSH setting: " & setting, - catSsh, svMedium - )) + result.findings.add( + makeFinding(configPath, "Weak SSH setting: " & setting, catSsh, svMedium) + ) proc scanAuthorizedKeys(config: HarvestConfig, result: var CollectorResult) = let akPath = expandHome(config, SshDir / SshAuthorizedKeys) @@ -133,11 +162,9 @@ proc scanAuthorizedKeys(config: HarvestConfig, result: var CollectorResult) = inc keyCount if keyCount > 0: - result.findings.add(makeFinding( - akPath, - $keyCount & " authorized public keys", - catSsh, svInfo - )) + result.findings.add( + makeFinding(akPath, $keyCount & " authorized public keys", catSsh, svInfo) + ) proc scanKnownHosts(config: HarvestConfig, result: var CollectorResult) = let khPath = expandHome(config, SshDir / SshKnownHosts) @@ -151,11 +178,9 @@ proc scanKnownHosts(config: HarvestConfig, result: var CollectorResult) = inc hostCount if hostCount > 0: - result.findings.add(makeFinding( - khPath, - $hostCount & " known hosts", - catSsh, svInfo - )) + result.findings.add( + makeFinding(khPath, $hostCount & " known hosts", catSsh, svInfo) + ) proc collect*(config: HarvestConfig): CollectorResult = result = newCollectorResult("ssh", catSsh) diff --git a/PROJECTS/intermediate/credential-enumeration/src/config.nim b/PROJECTS/intermediate/credential-enumeration/src/config.nim index 15f2619b..e53021df 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/config.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/config.nim @@ -1,5 +1,25 @@ # ©AngelaMos | 2026 # config.nim +# +# Application constants and default configuration +# +# Central repository for every configurable value in the tool. Defines +# the module category list with human-readable names and descriptions, +# filesystem paths for each collector target (Firefox/Chromium browser +# dirs, SSH paths and key headers/encryption markers, AWS/GCP/Azure/ +# Kubernetes config paths, shell history files, secret regex patterns, +# sensitive command patterns, .env file patterns, keyring/wallet/ +# password store dirs, Git credential files and token prefixes, and +# 20+ application token paths from Docker to Terraform to rclone). +# Also defines permission constants, ANSI color codes, severity color/ +# label mappings, box-drawing characters for terminal output, the +# ASCII banner, and the defaultConfig factory proc. +# +# Connects to: +# types.nim - Category, Severity, HarvestConfig, OutputFormat +# collectors/*.nim - all collectors reference path/pattern constants +# output/terminal.nim - banner, colors, box characters, severity labels +# harvester.nim - defaultConfig, ModuleNames, ModuleDescriptions {.push raises: [].} @@ -10,10 +30,8 @@ const AppVersion* = "0.1.0" BinaryName* = "credenum" - AllModules*: seq[Category] = @[ - catBrowser, catSsh, catCloud, - catHistory, catKeyring, catGit, catApptoken - ] + AllModules*: seq[Category] = + @[catBrowser, catSsh, catCloud, catHistory, catKeyring, catGit, catApptoken] ModuleNames*: array[Category, string] = [ catBrowser: "browser", @@ -22,7 +40,7 @@ const catHistory: "history", catKeyring: "keyring", catGit: "git", - catApptoken: "apptoken" + catApptoken: "apptoken", ] ModuleDescriptions*: array[Category, string] = [ @@ -32,7 +50,7 @@ const catHistory: "Shell history and environment files", catKeyring: "Keyrings and password stores", catGit: "Git credential stores", - catApptoken: "Application tokens and database configs" + catApptoken: "Application tokens and database configs", ] const @@ -42,12 +60,8 @@ const FirefoxCookiesDb* = "cookies.sqlite" FirefoxKeyDb* = "key4.db" - ChromiumDirs* = [ - ".config/google-chrome", - ".config/chromium", - ".config/brave", - ".config/vivaldi" - ] + ChromiumDirs* = + [".config/google-chrome", ".config/chromium", ".config/brave", ".config/vivaldi"] ChromiumLoginData* = "Login Data" ChromiumCookies* = "Cookies" ChromiumWebData* = "Web Data" @@ -59,20 +73,13 @@ const SshKnownHosts* = "known_hosts" SshKeyHeaders* = [ - "-----BEGIN OPENSSH PRIVATE KEY-----", - "-----BEGIN RSA PRIVATE KEY-----", - "-----BEGIN EC PRIVATE KEY-----", - "-----BEGIN DSA PRIVATE KEY-----", - "-----BEGIN PRIVATE KEY-----" + "-----BEGIN OPENSSH PRIVATE KEY-----", "-----BEGIN RSA PRIVATE KEY-----", + "-----BEGIN EC PRIVATE KEY-----", "-----BEGIN DSA PRIVATE KEY-----", + "-----BEGIN PRIVATE KEY-----", ] - SshEncryptedMarkers* = [ - "ENCRYPTED", - "Proc-Type: 4,ENCRYPTED", - "aes256-ctr", - "aes128-ctr", - "bcrypt" - ] + SshEncryptedMarkers* = + ["ENCRYPTED", "Proc-Type: 4,ENCRYPTED", "aes256-ctr", "aes128-ctr", "bcrypt"] SshSafeKeyPerms* = "0600" SshSafeDirPerms* = "0700" @@ -96,35 +103,17 @@ const KubeUserMarker* = "users:" const - HistoryFiles* = [ - ".bash_history", - ".zsh_history", - ".fish_history", - ".sh_history", - ".python_history" - ] + HistoryFiles* = + [".bash_history", ".zsh_history", ".fish_history", ".sh_history", ".python_history"] SecretPatterns* = [ - "KEY=", - "SECRET=", - "TOKEN=", - "PASSWORD=", - "PASSWD=", - "API_KEY=", - "ACCESS_KEY=", - "PRIVATE_KEY=", - "AUTH_TOKEN=", - "CREDENTIALS=" + "KEY=", "SECRET=", "TOKEN=", "PASSWORD=", "PASSWD=", "API_KEY=", "ACCESS_KEY=", + "PRIVATE_KEY=", "AUTH_TOKEN=", "CREDENTIALS=", ] HistoryCommandPatterns* = [ - "curl.*-h.*authoriz", - "curl.*-u ", - "wget.*--header.*authoriz", - "wget.*--password", - "mysql.*-p", - "psql.*password", - "sshpass" + "curl.*-h.*authoriz", "curl.*-u ", "wget.*--header.*authoriz", "wget.*--password", + "mysql.*-p", "psql.*password", "sshpass", ] EnvFileName* = ".env" @@ -206,7 +195,7 @@ const svLow: ColorCyan, svMedium: ColorYellow, svHigh: ColorBoldMagenta, - svCritical: ColorBoldRed + svCritical: ColorBoldRed, ] SeverityLabels*: array[Severity, string] = [ @@ -214,7 +203,7 @@ const svLow: "LOW", svMedium: "MEDIUM", svHigh: "HIGH", - svCritical: "CRITICAL" + svCritical: "CRITICAL", ] const @@ -241,5 +230,5 @@ proc defaultConfig*(): HarvestConfig = outputPath: "", dryRun: false, quiet: false, - verbose: false + verbose: false, ) diff --git a/PROJECTS/intermediate/credential-enumeration/src/harvester.nim b/PROJECTS/intermediate/credential-enumeration/src/harvester.nim index 30cf52e2..6c0dfd59 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/harvester.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/harvester.nim @@ -1,5 +1,23 @@ # ©AngelaMos | 2026 # harvester.nim +# +# CLI entry point and argument parser +# +# Parses command-line flags via std/parseopt (--target, --modules, +# --exclude, --format, --output, --dry-run, --quiet, --verbose, +# --help, --version) into a HarvestConfig. Dispatches to renderDryRun +# for --dry-run preview, otherwise calls runCollectors to execute all +# enabled modules, stamps the report with a UTC ISO 8601 timestamp, +# and routes output to renderTerminal, renderJson, or both. Exits +# with code 1 if any critical or high severity findings are detected, +# 0 otherwise. +# +# Connects to: +# config.nim - defaultConfig, AppVersion, ModuleNames/Descriptions +# types.nim - HarvestConfig, OutputFormat, Severity, Report +# runner.nim - runCollectors orchestrates module execution +# output/terminal.nim - renderTerminal for ANSI output +# output/json.nim - renderJson for structured output {.push raises: [].} @@ -21,7 +39,10 @@ proc printHelp() = stdout.writeLine "" stdout.writeLine ColorBold & "FLAGS:" & ColorReset stdout.writeLine " --target Target home directory (default: current user)" - stdout.writeLine " --modules Comma-separated modules: " & ModuleNames[catBrowser] & "," & ModuleNames[catSsh] & "," & ModuleNames[catCloud] & "," & ModuleNames[catHistory] & "," & ModuleNames[catKeyring] & "," & ModuleNames[catGit] & "," & ModuleNames[catApptoken] + stdout.writeLine " --modules Comma-separated modules: " & + ModuleNames[catBrowser] & "," & ModuleNames[catSsh] & "," & ModuleNames[catCloud] & + "," & ModuleNames[catHistory] & "," & ModuleNames[catKeyring] & "," & + ModuleNames[catGit] & "," & ModuleNames[catApptoken] stdout.writeLine " --exclude Comma-separated path patterns to skip" stdout.writeLine " --format Output format: terminal, json, both (default: terminal)" stdout.writeLine " --output Write JSON output to file" @@ -34,7 +55,8 @@ proc printHelp() = stdout.writeLine ColorBold & "EXAMPLES:" & ColorReset stdout.writeLine " " & BinaryName & " Scan current user" stdout.writeLine " " & BinaryName & " --format json JSON output" - stdout.writeLine " " & BinaryName & " --modules ssh,git,cloud Scan specific modules" + stdout.writeLine " " & BinaryName & + " --modules ssh,git,cloud Scan specific modules" stdout.writeLine " " & BinaryName & " --target /home/victim Scan another user" stdout.writeLine " " & BinaryName & " --dry-run Preview scan paths" stdout.writeLine "" @@ -63,7 +85,7 @@ proc parseCli(): HarvestConfig = var parser = initOptParser( commandLineParams(), shortNoVal = {'d', 'q', 'v', 'h'}, - longNoVal = @["dry-run", "quiet", "verbose", "help", "version"] + longNoVal = @["dry-run", "quiet", "verbose", "help", "version"], ) while true: @@ -111,7 +133,8 @@ proc renderDryRun(conf: HarvestConfig) = stdout.writeLine ColorBold & "Dry run — scan targets:" & ColorReset stdout.writeLine "" for cat in conf.enabledModules: - stdout.writeLine " " & ColorCyan & ModuleNames[cat] & ColorReset & ": " & ModuleDescriptions[cat] + stdout.writeLine " " & ColorCyan & ModuleNames[cat] & ColorReset & ": " & + ModuleDescriptions[cat] stdout.writeLine "" stdout.writeLine ColorDim & " Target: " & conf.targetDir & ColorReset stdout.writeLine "" diff --git a/PROJECTS/intermediate/credential-enumeration/src/output/json.nim b/PROJECTS/intermediate/credential-enumeration/src/output/json.nim index 9a179341..ee8b0f45 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/output/json.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/output/json.nim @@ -1,5 +1,27 @@ # ©AngelaMos | 2026 # json.nim +# +# JSON report serializer +# +# Converts the Report object tree into a structured JSON document +# using std/json's JsonNode builders. credentialToJson serializes a +# Credential with source, type, preview, and a metadata key-value +# map. findingToJson serializes a Finding with path, category, +# severity, description, permissions, modified timestamp, file +# size, and an optional nested credential object. +# collectorResultToJson wraps a module's findings array alongside +# its name, category, duration, and error list. reportToJson +# assembles the top-level structure: metadata block (timestamp, +# target directory, version, duration, module list), a modules +# array of collector results, and a summary object mapping each +# Severity level to its finding count. renderJson pretty-prints the +# JSON tree to stdout and optionally writes to a file path. +# All builder procs use {.cast(raises: []).} to suppress exception +# tracking within the JSON construction blocks. +# +# Connects to: +# types.nim - Credential, Finding, CollectorResult, Report, +# Severity, ReportMetadata {.push raises: [].} diff --git a/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim b/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim index 630ba646..e12b0335 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim @@ -1,5 +1,37 @@ # ©AngelaMos | 2026 # terminal.nim +# +# ANSI terminal renderer with box-drawing output +# +# Renders the full credential enumeration report to the terminal +# using Unicode box-drawing characters for bordered sections. +# visualLen computes display width by skipping ANSI escape +# sequences and UTF-8 continuation bytes so padding aligns +# correctly despite embedded color codes. truncateVisual truncates +# strings at a visual-width boundary without splitting escape +# sequences. sevBadge produces colored severity labels using the +# SeverityColors and SeverityLabels maps from config. Three box +# helpers (boxLine, boxBottom, boxMid) draw top, bottom, and +# mid-section borders at a fixed 78-column width. +# renderModuleHeader prints a bordered header with the module name, +# category description, finding count, and duration. renderFinding +# shows a severity badge, truncated description, file path with +# permissions and modification date, and an optional credential +# preview. renderSummary totals findings across all modules and +# displays a severity badge breakdown. renderTerminal orchestrates +# the full output sequence: banner, target and module metadata, +# per-module sections (skipping empty modules unless verbose), and +# the summary footer. +# +# Connects to: +# types.nim - Severity, Finding, Report, CollectorResult, +# Credential, ReportMetadata +# config.nim - BoxVertical, BoxTopLeft, BoxTopRight, BoxBottomLeft, +# BoxBottomRight, BoxHorizontal, BoxTeeRight, BoxTeeLeft, +# SeverityColors, SeverityLabels, ColorBold, ColorReset, +# ColorDim, ColorCyan, ColorBoldRed, Banner, +# BannerTagline, AppVersion, Arrow, CrossMark, +# ModuleDescriptions {.push raises: [].} @@ -88,14 +120,16 @@ proc renderBanner*(quiet: bool) = except CatchableError: discard -proc renderModuleHeader(name: string, desc: string, findingCount: int, durationMs: int64) = +proc renderModuleHeader( + name: string, desc: string, findingCount: int, durationMs: int64 +) = try: stdout.writeLine boxLine(BoxWidth) - let label = BoxVertical & " " & ColorBold & ColorCyan & - name.toUpperAscii() & ColorReset & ColorDim & " " & Arrow & - " " & desc & ColorReset - let stats = $findingCount & " findings" & ColorDim & " (" & - $durationMs & "ms)" & ColorReset + let label = + BoxVertical & " " & ColorBold & ColorCyan & name.toUpperAscii() & ColorReset & + ColorDim & " " & Arrow & " " & desc & ColorReset + let stats = + $findingCount & " findings" & ColorDim & " (" & $durationMs & "ms)" & ColorReset let usedWidth = 2 + name.len + 3 + desc.len let statsVisual = visualLen(stats) let gap = BoxWidth - usedWidth - statsVisual - 2 @@ -111,12 +145,12 @@ proc renderModuleHeader(name: string, desc: string, findingCount: int, durationM discard proc renderFinding(f: Finding) = - let descLine = BoxVertical & " " & sevBadge(f.severity) & " " & + let descLine = + BoxVertical & " " & sevBadge(f.severity) & " " & truncateVisual(f.description, InnerWidth - 14) writeBoxLine(descLine) - var detail = BoxVertical & " " & ColorDim & f.path & - " [" & f.permissions & "]" + var detail = BoxVertical & " " & ColorDim & f.path & " [" & f.permissions & "]" if f.modified != "unknown": detail &= " mod:" & f.modified detail &= ColorReset @@ -125,16 +159,17 @@ proc renderFinding(f: Finding) = if f.credential.isSome: let cred = f.credential.get() if cred.preview.len > 0: - let previewLine = BoxVertical & " " & ColorDim & Arrow & - " " & cred.preview & ColorReset + let previewLine = + BoxVertical & " " & ColorDim & Arrow & " " & cred.preview & ColorReset writeBoxLine(previewLine) proc renderModuleErrors(errors: seq[string]) = if errors.len == 0: return for err in errors: - let errLine = BoxVertical & " " & ColorBoldRed & CrossMark & - ColorReset & " " & ColorDim & err & ColorReset + let errLine = + BoxVertical & " " & ColorBoldRed & CrossMark & ColorReset & " " & ColorDim & err & + ColorReset writeBoxLine(errLine) proc renderSummary(report: Report) = @@ -148,10 +183,10 @@ proc renderSummary(report: Report) = for sev in Severity: totalFindings += report.summary[sev] - let countLine = BoxVertical & " " & ColorBold & $totalFindings & - ColorReset & " findings across " & ColorBold & - $report.results.len & ColorReset & " modules" & ColorDim & - " (" & $report.metadata.durationMs & "ms)" & ColorReset + let countLine = + BoxVertical & " " & ColorBold & $totalFindings & ColorReset & " findings across " & + ColorBold & $report.results.len & ColorReset & " modules" & ColorDim & " (" & + $report.metadata.durationMs & "ms)" & ColorReset writeBoxLine(countLine) var badgeLine = BoxVertical & " " @@ -184,10 +219,7 @@ proc renderTerminal*(report: Report, quiet: bool, verbose: bool) = continue renderModuleHeader( - res.name, - ModuleDescriptions[res.category], - res.findings.len, - res.durationMs + res.name, ModuleDescriptions[res.category], res.findings.len, res.durationMs ) for finding in res.findings: diff --git a/PROJECTS/intermediate/credential-enumeration/src/runner.nim b/PROJECTS/intermediate/credential-enumeration/src/runner.nim index 1b325cb4..2e4897cf 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/runner.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/runner.nim @@ -1,5 +1,25 @@ # ©AngelaMos | 2026 # runner.nim +# +# Module dispatcher and report assembler +# +# Maps each Category enum value to its collector proc via getCollector, +# then runCollectors iterates the enabled modules from HarvestConfig, +# invokes each collector, collects results with monotonic timing, and +# builds the final Report with aggregated severity counts across all +# findings. The metadata timestamp is left empty for the caller +# (harvester.nim) to fill with wall-clock time. +# +# Connects to: +# types.nim - HarvestConfig, Report, CollectorResult, Severity +# config.nim - AppVersion, ModuleNames +# collectors/ssh.nim - ssh.collect +# collectors/git.nim - git.collect +# collectors/cloud.nim - cloud.collect +# collectors/browser.nim - browser.collect +# collectors/history.nim - history.collect +# collectors/keyring.nim - keyring.collect +# collectors/apptoken.nim - apptoken.collect {.push raises: [].} @@ -49,8 +69,8 @@ proc runCollectors*(config: HarvestConfig): Report = target: config.targetDir, version: AppVersion, durationMs: elapsed.inMilliseconds, - modules: moduleNames + modules: moduleNames, ), results: results, - summary: summary + summary: summary, ) diff --git a/PROJECTS/intermediate/credential-enumeration/src/types.nim b/PROJECTS/intermediate/credential-enumeration/src/types.nim index 05b69acc..4b207871 100644 --- a/PROJECTS/intermediate/credential-enumeration/src/types.nim +++ b/PROJECTS/intermediate/credential-enumeration/src/types.nim @@ -1,5 +1,24 @@ # ©AngelaMos | 2026 # types.nim +# +# Domain types for the credential enumeration tool +# +# Defines the core type hierarchy: Severity (info through critical) and +# Category (browser, ssh, cloud, history, keyring, git, apptoken) as +# string-backed enums. Finding captures a discovered credential exposure +# with path, severity, description, optional Credential detail, +# permissions, modification time, and file size. CollectorResult groups +# findings from a single module with timing and error tracking. Report +# aggregates all collector results with metadata (timestamp, target, +# version, duration, module list) and a severity summary array. +# HarvestConfig holds CLI-parsed runtime options. CollectorProc defines +# the nimcall signature all collector modules implement. +# +# Connects to: +# config.nim - constructs HarvestConfig via defaultConfig +# collectors/base.nim - makeFinding/makeFindingWithCred build Findings +# output/json.nim - serializes Report/Finding/Credential to JSON +# output/terminal.nim - renders Report/Finding with severity badges {.push raises: [].} diff --git a/PROJECTS/intermediate/credential-enumeration/tests/docker/validate.sh b/PROJECTS/intermediate/credential-enumeration/tests/docker/validate.sh old mode 100644 new mode 100755 index 13aa54e7..87d1ed58 --- a/PROJECTS/intermediate/credential-enumeration/tests/docker/validate.sh +++ b/PROJECTS/intermediate/credential-enumeration/tests/docker/validate.sh @@ -1,6 +1,30 @@ #!/usr/bin/env bash # ©AngelaMos | 2026 # validate.sh +# +# Docker-based integration test for all 7 collector categories +# +# Runs the credenum binary against planted test fixtures under +# /home/testuser and validates that every expected finding appears +# in the output. Captures JSON-format output into OUTPUT, then +# runs the terminal renderer for visual inspection. The check() +# helper greps the captured output for a case-insensitive pattern +# and tallies pass/fail counts. +# +# Validates 30 findings across all categories: ssh (unprotected +# key, encrypted key, weak config, authorized keys, known hosts), +# cloud (AWS static keys, AWS config, GCP service account, +# Kubernetes config), browser (Firefox logins, cookies, key +# database, Chromium login data), history (secret pattern, curl +# auth, sshpass, environment file), keyring (GNOME Keyring, +# KeePass database, password store), git (plaintext credentials, +# credential helper, GitHub token), apptoken (PostgreSQL, MySQL, +# Docker auth, netrc, npm, PyPI, GitHub CLI, Vault). Exits with +# code 1 if any check fails. +# +# Connects to: +# credenum binary - all 7 collector modules +# tests/docker/Dockerfile - fixture layout in /home/testuser set -euo pipefail diff --git a/PROJECTS/intermediate/credential-enumeration/tests/test_all.nim b/PROJECTS/intermediate/credential-enumeration/tests/test_all.nim index 8fb5df05..8acf0c85 100644 --- a/PROJECTS/intermediate/credential-enumeration/tests/test_all.nim +++ b/PROJECTS/intermediate/credential-enumeration/tests/test_all.nim @@ -1,5 +1,36 @@ # ©AngelaMos | 2026 # test_all.nim +# +# Unit tests for core utility and parsing functions +# +# Exercises exported helpers from four modules across eight test +# suites. redactValue covers short, long, exact-length, and empty +# strings. isPrivateKey validates detection of five PEM header +# formats (OpenSSH, RSA, ECDSA, DSA, PKCS8) and rejection of +# public keys and non-key content. isEncrypted checks for +# ENCRYPTED, bcrypt, and aes256-ctr markers versus unencrypted +# keys. matchesSecretPattern verifies detection of export-prefixed +# and bare KEY=/SECRET=/TOKEN=/PASSWORD= assignments while +# rejecting PATH exports and ordinary commands. +# matchesCommandPattern tests curl with auth headers and -u flag, +# wget with authorization header and password, mysql -p, psql +# password, and sshpass detection, rejecting safe commands. +# matchesExclude validates exact filename and directory segment +# matching without false positives on partial or embedded +# substrings. permissionSeverity confirms svInfo for nonexistent +# paths. parseModules tests single, multiple, whitespace-padded, +# full-set, empty, and unknown module string parsing. redactLine +# checks export-prefixed quoted, unquoted, and single-quoted value +# redaction plus passthrough for lines without an equals sign. +# +# Connects to: +# types.nim - Category enum values for parseModules +# collectors/base.nim - redactValue, matchesExclude, +# permissionSeverity +# collectors/ssh.nim - isPrivateKey, isEncrypted +# collectors/history.nim - matchesSecretPattern, +# matchesCommandPattern, redactLine +# harvester.nim - parseModules import std/[unittest, strutils] import types diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/.gitignore b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/.gitignore new file mode 100644 index 00000000..faf22360 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/.gitignore @@ -0,0 +1,7 @@ +bin/ +coverage.out +coverage.html +*.db +.bomber/ +dist/ +docs/ diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/.golangci.yml b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/.golangci.yml new file mode 100644 index 00000000..794b0097 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/.golangci.yml @@ -0,0 +1,31 @@ +# ©AngelaMos | 2026 +# .golangci.yml + +run: + timeout: 5m + +linters: + enable: + - errcheck + - govet + - staticcheck + - unused + - gosimple + - ineffassign + - typecheck + - gofmt + - gocritic + - gosec + - misspell + - unconvert + - unparam + - prealloc + +linters-settings: + gocritic: + enabled-tags: + - diagnostic + - performance + gosec: + excludes: + - G304 diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/Justfile b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/Justfile new file mode 100644 index 00000000..1d70b942 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/Justfile @@ -0,0 +1,107 @@ +# ©AngelaMos | 2026 +# Justfile + +set export +set shell := ["bash", "-uc"] + +project := file_name(justfile_directory()) +version := `git describe --tags --always 2>/dev/null || echo "dev"` + +default: + @just --list --unsorted + +[group('lint')] +lint *ARGS: + golangci-lint run --timeout=5m {{ARGS}} + +[group('lint')] +lint-fix: + golangci-lint run --timeout=5m --fix + +[group('lint')] +format: + golangci-lint fmt + +[group('lint')] +tidy: + go mod tidy + +[group('lint')] +vet: + go vet ./... + +[group('test')] +test *ARGS: + go test -race ./... {{ARGS}} + +[group('test')] +test-v *ARGS: + go test -race -v ./... {{ARGS}} + +[group('test')] +cover: + go test -race -cover ./... + +[group('test')] +cover-html: + go test -race -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report: coverage.html" + +[group('ci')] +ci: lint test + @echo "All checks passed." + +[group('ci')] +check: lint vet + +[group('dev')] +run *ARGS: + go run ./cmd/bomber {{ARGS}} + +[group('dev')] +dev-scan: + go run ./cmd/bomber scan . + +[group('dev')] +dev-generate: + go run ./cmd/bomber generate . --format cyclonedx + +[group('dev')] +dev-vuln: + go run ./cmd/bomber vuln . + +[group('dev')] +dev-check: + go run ./cmd/bomber check . --policy policy.yaml + +[group('prod')] +build: + go build -ldflags="-s -w" -o bin/bomber ./cmd/bomber + @echo "Built: bin/bomber ($(du -h bin/bomber | cut -f1))" + +[group('prod')] +build-debug: + go build -o bin/bomber ./cmd/bomber + +[group('prod')] +install: + go install ./cmd/bomber + +[group('util')] +info: + @echo "Project: {{project}}" + @echo "Version: {{version}}" + @echo "Go: $(go version | cut -d' ' -f3)" + @echo "OS: {{os()}} ({{arch()}})" + @echo "Module: $(head -1 go.mod | cut -d' ' -f2)" + +[group('util')] +update: + go get -u ./... + go mod tidy + +[group('util')] +clean: + -rm -rf bin/ coverage.out coverage.html + @echo "Cleaned build artifacts." diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/LICENSE b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/README.md b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/README.md new file mode 100644 index 00000000..b421da58 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/README.md @@ -0,0 +1,147 @@ +```regex +██████╗ ██████╗ ███╗ ███╗██████╗ ███████╗██████╗ +██╔══██╗██╔═══██╗████╗ ████║██╔══██╗██╔════╝██╔══██╗ +██████╔╝██║ ██║██╔████╔██║██████╔╝█████╗ ██████╔╝ +██╔══██╗██║ ██║██║╚██╔╝██║██╔══██╗██╔══╝ ██╔══██╗ +██████╔╝╚██████╔╝██║ ╚═╝ ██║██████╔╝███████╗██║ ██║ +╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ +``` + +[![Cybersecurity Projects](https://img.shields.io/badge/Cybersecurity--Projects-Project%20%2340-red?style=flat&logo=github)](https://github.com/CarterPerez-dev/Cybersecurity-Projects/tree/main/PROJECTS/intermediate/sbom-generator-vulnerability-matcher) +[![Go](https://img.shields.io/badge/Go-1.25-00ADD8?style=flat&logo=go&logoColor=white)](https://go.dev) +[![License: AGPLv3](https://img.shields.io/badge/License-AGPL_v3-purple.svg)](https://www.gnu.org/licenses/agpl-3.0) + +> SBOM generator and vulnerability matcher that scans Go, Node.js, and Python projects, produces SPDX 2.3 and CycloneDX 1.5 documents, and cross-references packages against OSV and NVD vulnerability databases. + +*This is a quick overview — security theory, architecture, and full walkthroughs are in the [learn modules](#learn).* + +## What It Does + +- Multi-ecosystem dependency scanning (Go `go.mod`/`go.sum`, Node.js `package.json`/`pnpm-lock.yaml`, Python `pyproject.toml`/`uv.lock`) +- Dependency graph construction with cycle detection and depth tracking +- SBOM generation in SPDX 2.3 and CycloneDX 1.5 JSON formats +- Vulnerability matching via OSV batch API (primary) and NVD REST API (optional) +- SQLite-backed response cache with configurable TTL +- Policy engine for CI/CD gates with severity thresholds and dependency depth limits +- Monorepo support with recursive ecosystem detection + +## Quick Start + +```bash +go install github.com/CarterPerez-dev/bomber/cmd/bomber@latest +``` + +Or use the install script: + +```bash +curl -fsSL https://raw.githubusercontent.com/CarterPerez-dev/Cybersecurity-Projects/main/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/install.sh | bash +``` + +> [!TIP] +> This project uses [`just`](https://github.com/casey/just) as a command runner. Type `just` to see all available commands. +> +> Install: `curl -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin` + +### Usage + +```bash +bomber scan ./my-project # scan dependencies +bomber generate ./my-project --format spdx # SPDX 2.3 SBOM +bomber generate ./my-project --format cyclonedx # CycloneDX 1.5 SBOM +bomber vuln ./my-project # vulnerability scan +bomber check ./my-project --policy policy.yaml # CI/CD policy gate +``` + +### Policy File + +```yaml +max_severity: medium +max_depth: 5 +``` + +`bomber check` exits with code 1 when violations are found — drop it into any CI pipeline. + +## Supported Ecosystems + +| Ecosystem | Manifest | Lockfile | +|-----------|----------|----------| +| Go | `go.mod` | `go.sum` | +| Node.js | `package.json` | `pnpm-lock.yaml` | +| Python | `pyproject.toml` | `uv.lock` | + +## Architecture + +``` +bomber scan ./project/ + +┌─────────────────────────────────────────────────┐ +│ CLI (cobra) │ +│ scan • generate • vuln • check │ +└──────────────────────┬──────────────────────────┘ + │ + ┌─────────────▼─────────────┐ + │ Scanner Engine │ + │ walks dir, detects │ + │ ecosystems, dispatches │ + └─────────────┬─────────────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + ┌─────────┐ ┌─────────┐ ┌─────────┐ + │Go Parser│ │Node │ │Python │ + │go.mod │ │pnpm-lock│ │uv.lock │ + │go.sum │ │pkg.json │ │pyproject│ + └────┬────┘ └────┬────┘ └────┬────┘ + │ │ │ + └──────────────┼──────────────┘ + ▼ + ┌─────────────────────────┐ + │ Dependency Graph │ + │ direct + transitive │ + │ cycle detection │ + └────────────┬────────────┘ + │ + ┌─────────┼─────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ SPDX 2.3 │ │ CycloneDX │ + │ Generator │ │ 1.5 Generator│ + └──────┬───────┘ └──────┬───────┘ + │ │ + └─────────┬─────────┘ + ▼ + ┌─────────────────────┐ + │ Vulnerability │ + │ Matcher │ + │ OSV API (primary) │ + │ NVD API (optional) │ + └─────────┬───────────┘ + ▼ + ┌─────────────────────┐ + │ Policy Engine │ + │ --check mode │ + │ exit code 0 or 1 │ + └─────────────────────┘ +``` + +## Stack + +**Language:** Go 1.25 + +**Dependencies:** cobra (CLI), fatih/color (terminal), go-toml/v2 (TOML), yaml.v3 (YAML), modernc.org/sqlite (cache), google/uuid (CycloneDX), testify (tests) + +## Learn + +This project includes step-by-step learning materials covering security theory, architecture, and implementation. + +| Module | Topic | +|--------|-------| +| [00 - Overview](learn/00-OVERVIEW.md) | Prerequisites and quick start | +| [01 - Concepts](learn/01-CONCEPTS.md) | SBOMs, supply chain security, and vulnerability databases | +| [02 - Architecture](learn/02-ARCHITECTURE.md) | System design and data flow | +| [03 - Implementation](learn/03-IMPLEMENTATION.md) | Code walkthrough | +| [04 - Challenges](learn/04-CHALLENGES.md) | Extension ideas and exercises | + +## License + +AGPL 3.0 diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/cmd/bomber/main.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/cmd/bomber/main.go new file mode 100644 index 00000000..43a543f0 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/cmd/bomber/main.go @@ -0,0 +1,10 @@ +// ©AngelaMos | 2026 +// main.go + +package main + +import "github.com/CarterPerez-dev/bomber/internal/cli" + +func main() { + cli.Execute() +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/go.mod b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/go.mod new file mode 100644 index 00000000..d93ced9a --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/go.mod @@ -0,0 +1,29 @@ +module github.com/CarterPerez-dev/bomber + +go 1.25.0 + +require ( + github.com/fatih/color v1.19.0 + github.com/google/uuid v1.6.0 + github.com/pelletier/go-toml/v2 v2.3.0 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.48.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/spf13/pflag v1.0.9 // indirect + golang.org/x/sys v0.42.0 // indirect + modernc.org/libc v1.70.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/go.sum b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/go.sum new file mode 100644 index 00000000..124a343a --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/go.sum @@ -0,0 +1,76 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA= +modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/install.sh b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/install.sh new file mode 100755 index 00000000..d7ee3690 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/install.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# ©AngelaMos | 2026 +# install.sh + +set -euo pipefail + +REPO_OWNER="CarterPerez-dev" +REPO_NAME="bomber" +BINARY="bomber" +INSTALL_DIR="${BOMBER_INSTALL_DIR:-$HOME/.bomber/bin}" +VERSION="${BOMBER_VERSION:-}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +info() { echo -e " ${GREEN}+${NC} $1"; } +warn() { echo -e " ${YELLOW}!${NC} $1"; } +fail() { echo -e " ${RED}x${NC} $1"; exit 1; } +header() { echo -e "\n${BOLD}${CYAN}--- $1 ---${NC}\n"; } + +TMP_DIR="" +cleanup() { [[ -n "$TMP_DIR" ]] && rm -rf "$TMP_DIR"; } +trap cleanup EXIT + +echo -e "${BOLD}" +echo -e " ${RED} ██▄ ▄▀▄ █▄ ▄█ ██▄ ██▀ █▀▄ ${NC}" +echo -e " ${CYAN} █▄█ ▀▄▀ █ ▀ █ █▄█ █▄▄ █▀▄ ${NC}" +echo -e "${NC}" +echo -e " ${DIM}SBOM generator & vulnerability matcher${NC}" + +header "Detecting system" + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Linux) OS="linux" ;; + Darwin) OS="darwin" ;; + MINGW*|MSYS*|CYGWIN*) fail "Windows is not supported. Use: go install github.com/${REPO_OWNER}/${REPO_NAME}/cmd/bomber@latest" ;; + *) fail "Unsupported OS: $OS" ;; +esac + +case "$ARCH" in + x86_64|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) fail "Unsupported architecture: $ARCH" ;; +esac + +info "System: ${OS}/${ARCH}" + +if [[ -z "$VERSION" ]]; then + header "Fetching latest release" + + if command -v curl &>/dev/null; then + VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" 2>/dev/null \ + | grep '"tag_name":' \ + | sed -E 's/.*"([^"]+)".*/\1/') || true + elif command -v wget &>/dev/null; then + VERSION=$(wget -qO- "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" 2>/dev/null \ + | grep '"tag_name":' \ + | sed -E 's/.*"([^"]+)".*/\1/') || true + fi +fi + +INSTALLED=false + +if [[ -n "$VERSION" ]]; then + info "Version: ${VERSION}" + header "Downloading pre-built binary" + + ARCHIVE="${BINARY}_${VERSION#v}_${OS}_${ARCH}.tar.gz" + URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${VERSION}/${ARCHIVE}" + + TMP_DIR=$(mktemp -d) + + DOWNLOAD_OK=false + if command -v curl &>/dev/null; then + curl -fsSL "$URL" -o "$TMP_DIR/archive.tar.gz" 2>/dev/null && DOWNLOAD_OK=true + elif command -v wget &>/dev/null; then + wget -q "$URL" -O "$TMP_DIR/archive.tar.gz" 2>/dev/null && DOWNLOAD_OK=true + else + fail "Neither curl nor wget found" + fi + + if [[ "$DOWNLOAD_OK" == "true" ]]; then + tar -xzf "$TMP_DIR/archive.tar.gz" -C "$TMP_DIR" + mkdir -p "$INSTALL_DIR" + mv "$TMP_DIR/$BINARY" "$INSTALL_DIR/" + chmod +x "$INSTALL_DIR/$BINARY" + INSTALLED=true + info "Installed to ${INSTALL_DIR}/${BINARY}" + else + warn "Pre-built binary not available for ${OS}/${ARCH}" + fi +fi + +if [[ "$INSTALLED" == "false" ]]; then + if command -v go &>/dev/null; then + GO_VER=$(go version | awk '{print $3}') + header "Building from source (${GO_VER})" + info "Running go install..." + GOBIN="$INSTALL_DIR" go install "github.com/${REPO_OWNER}/${REPO_NAME}/cmd/bomber@latest" + INSTALLED=true + info "Installed to ${INSTALL_DIR}/${BINARY}" + else + echo "" + fail "No pre-built binary and Go is not installed. + + Option 1 — Install Go, then: + go install github.com/${REPO_OWNER}/${REPO_NAME}/cmd/bomber@latest + + Option 2 — Install Go: + https://go.dev/dl/" + fi +fi + +header "Configuring PATH" + +PATH_UPDATED=false + +case ":$PATH:" in + *":${INSTALL_DIR}:"*) + info "${INSTALL_DIR} already in PATH" + PATH_UPDATED=true + ;; +esac + +if [[ "$PATH_UPDATED" == "false" ]]; then + CURRENT_SHELL="$(basename "${SHELL:-/bin/bash}")" + TARGET="" + + case "$CURRENT_SHELL" in + zsh) + [[ -f "$HOME/.zshrc" ]] && TARGET="$HOME/.zshrc" + ;; + bash) + if [[ -f "$HOME/.bashrc" ]]; then + TARGET="$HOME/.bashrc" + elif [[ -f "$HOME/.bash_profile" ]]; then + TARGET="$HOME/.bash_profile" + fi + ;; + fish) + mkdir -p "$HOME/.config/fish/conf.d" + echo "set -gx PATH \"$INSTALL_DIR\" \$PATH" > "$HOME/.config/fish/conf.d/bomber.fish" + info "Added to ~/.config/fish/conf.d/bomber.fish" + PATH_UPDATED=true + ;; + esac + + if [[ "$PATH_UPDATED" == "false" && -z "${TARGET:-}" ]]; then + [[ -f "$HOME/.profile" ]] && TARGET="$HOME/.profile" + fi + + if [[ "$PATH_UPDATED" == "false" && -n "${TARGET:-}" ]]; then + if ! grep -q "$INSTALL_DIR" "$TARGET" 2>/dev/null; then + printf '\nexport PATH="%s:$PATH"\n' "$INSTALL_DIR" >> "$TARGET" + info "Added to ${TARGET}" + else + info "Already configured in ${TARGET}" + fi + fi +fi + +echo "" +echo -e " ${GREEN}${BOLD}bomber installed successfully${NC}" +echo "" + +if ! command -v bomber &>/dev/null; then + warn "Restart your shell or run:" + echo -e " ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${NC}" + echo "" +fi + +echo -e " ${DIM}Quick start:${NC}" +echo "" +echo -e " ${CYAN}bomber scan [path]${NC} Scan dependencies" +echo -e " ${CYAN}bomber generate [path] --format spdx${NC} Generate SPDX SBOM" +echo -e " ${CYAN}bomber vuln [path]${NC} Check for vulnerabilities" +echo -e " ${CYAN}bomber check [path] --policy p.yaml${NC} CI/CD policy gate" +echo "" +echo -e " ${DIM}Docs: https://github.com/${REPO_OWNER}/Cybersecurity-Projects${NC}" +echo "" diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/check.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/check.go new file mode 100644 index 00000000..089d19e2 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/check.go @@ -0,0 +1,85 @@ +// ©AngelaMos | 2026 +// check.go + +package cli + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/CarterPerez-dev/bomber/internal/parser" + "github.com/CarterPerez-dev/bomber/internal/policy" + "github.com/CarterPerez-dev/bomber/internal/report" + "github.com/CarterPerez-dev/bomber/internal/scanner" + "github.com/CarterPerez-dev/bomber/internal/ui" +) + +var policyFile string + +var checkCmd = &cobra.Command{ + Use: "check [path]", + Short: "Evaluate SBOM against a policy", + Args: cobra.MaximumNArgs(1), + RunE: runCheck, +} + +func init() { + checkCmd.Flags().StringVarP( + &policyFile, "policy", "p", "policy.yaml", + "policy file path", + ) +} + +func runCheck(cmd *cobra.Command, args []string) error { + path := "." + if len(args) > 0 { + path = args[0] + } + + pol, err := policy.LoadPolicy(policyFile) + if err != nil { + return fmt.Errorf("load policy: %w", err) + } + + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := scanner.New(reg) + result, err := s.Scan(path) + if err != nil { + return err + } + + sp := ui.NewSpinner("Querying vulnerability databases...") + if formatFlag != "json" { + sp.Start() + } + + vulnReport, err := queryVulns(cmd.Context(), result) + + if formatFlag != "json" { + sp.Stop() + } + if err != nil { + return err + } + + checkResult := policy.Evaluate(pol, vulnReport, result.Graphs) + + if formatFlag == "json" { + return report.WriteJSON(os.Stdout, result, vulnReport, checkResult) + } + + ui.PrintBanner() + report.PrintScanSummary(os.Stdout, result) + report.PrintVulnReport(os.Stdout, vulnReport) + report.PrintCheckResult(os.Stdout, checkResult) + + if !checkResult.Passed { + os.Exit(1) + } + + return nil +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/generate.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/generate.go new file mode 100644 index 00000000..e6b54e5d --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/generate.go @@ -0,0 +1,86 @@ +// ©AngelaMos | 2026 +// generate.go + +package cli + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/CarterPerez-dev/bomber/internal/parser" + "github.com/CarterPerez-dev/bomber/internal/sbom" + "github.com/CarterPerez-dev/bomber/internal/scanner" + "github.com/CarterPerez-dev/bomber/internal/ui" +) + +var ( + outputFile string + sbomFormatFlag string +) + +var generateCmd = &cobra.Command{ + Use: "generate [path]", + Short: "Generate an SBOM document", + Args: cobra.MaximumNArgs(1), + RunE: runGenerate, +} + +func init() { + generateCmd.Flags().StringVarP( + &outputFile, "output", "o", "", + "output file (default: stdout)", + ) + generateCmd.Flags().StringVar( + &sbomFormatFlag, "sbom-format", "cyclonedx", + "SBOM format: spdx, cyclonedx", + ) +} + +func runGenerate(_ *cobra.Command, args []string) error { + path := "." + if len(args) > 0 { + path = args[0] + } + + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := scanner.New(reg) + result, err := s.Scan(path) + if err != nil { + return err + } + + if len(result.Graphs) == 0 { + return fmt.Errorf("no ecosystems detected in %s", path) + } + + var data []byte + switch sbomFormatFlag { + case "spdx": + gen := sbom.NewSPDXGenerator() + data, err = gen.Generate(result.Graphs) + case "cyclonedx": + gen := sbom.NewCycloneDXGenerator() + data, err = gen.Generate(result.Graphs) + default: + return fmt.Errorf("unknown SBOM format: %s (use spdx or cyclonedx)", sbomFormatFlag) + } + if err != nil { + return err + } + + if outputFile != "" { + if err := os.WriteFile(outputFile, data, 0o644); err != nil { + return fmt.Errorf("write output: %w", err) + } + fmt.Fprintf(os.Stderr, " %s SBOM written to %s (%s)\n", + ui.Check, outputFile, sbomFormatFlag) + return nil + } + + _, err = os.Stdout.Write(data) + return err +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/root.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/root.go new file mode 100644 index 00000000..cca51a51 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/root.go @@ -0,0 +1,97 @@ +// ©AngelaMos | 2026 +// root.go + +package cli + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + "github.com/CarterPerez-dev/bomber/internal/config" + "github.com/CarterPerez-dev/bomber/internal/ui" +) + +var ( + formatFlag string + verbose bool + noColor bool + noCache bool +) + +var rootCmd = &cobra.Command{ + Use: "bomber", + Short: "SBOM generator & vulnerability matcher", + Version: config.ToolVersion, + Long: `Bomber scans project directories for dependencies, generates SBOM +documents in SPDX 2.3 and CycloneDX 1.5 formats, and cross-references +packages against vulnerability databases.`, + SilenceUsage: true, + SilenceErrors: true, +} + +func Execute() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "%s %s\n", + ui.Cross, ui.Red(err.Error())) + os.Exit(1) + } +} + +func run() error { + ctx, cancel := signal.NotifyContext( + context.Background(), os.Interrupt, syscall.SIGTERM, + ) + defer cancel() + + return rootCmd.ExecuteContext(ctx) +} + +func init() { + cobra.OnInitialize(initGlobals) + + rootCmd.PersistentFlags().StringVarP( + &formatFlag, "format", "f", "terminal", + "output format: terminal, json", + ) + rootCmd.PersistentFlags().BoolVarP( + &verbose, "verbose", "v", false, + "verbose output", + ) + rootCmd.PersistentFlags().BoolVar( + &noColor, "no-color", false, + "disable colored output", + ) + rootCmd.PersistentFlags().BoolVar( + &noCache, "no-cache", false, + "disable vulnerability cache", + ) + + rootCmd.AddCommand(scanCmd) + rootCmd.AddCommand(generateCmd) + rootCmd.AddCommand(vulnCmd) + rootCmd.AddCommand(checkCmd) + + defaultHelp := rootCmd.HelpFunc() + rootCmd.SetHelpFunc( + func(cmd *cobra.Command, args []string) { + if cmd.Root() == cmd { + ui.PrintBannerWithArt() + } else { + ui.PrintBanner() + } + defaultHelp(cmd, args) + }, + ) +} + +func initGlobals() { + if noColor { + color.NoColor = true + } +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/scan.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/scan.go new file mode 100644 index 00000000..922f49dd --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/scan.go @@ -0,0 +1,46 @@ +// ©AngelaMos | 2026 +// scan.go + +package cli + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/CarterPerez-dev/bomber/internal/parser" + "github.com/CarterPerez-dev/bomber/internal/report" + "github.com/CarterPerez-dev/bomber/internal/scanner" + "github.com/CarterPerez-dev/bomber/internal/ui" +) + +var scanCmd = &cobra.Command{ + Use: "scan [path]", + Short: "Scan a directory for dependencies", + Args: cobra.MaximumNArgs(1), + RunE: runScan, +} + +func runScan(_ *cobra.Command, args []string) error { + path := "." + if len(args) > 0 { + path = args[0] + } + + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := scanner.New(reg) + result, err := s.Scan(path) + if err != nil { + return err + } + + if formatFlag == "json" { + return report.WriteJSON(os.Stdout, result, nil, nil) + } + + ui.PrintBanner() + report.PrintScanSummary(os.Stdout, result) + return nil +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/vuln.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/vuln.go new file mode 100644 index 00000000..b9490a78 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/cli/vuln.go @@ -0,0 +1,189 @@ +// ©AngelaMos | 2026 +// vuln.go + +package cli + +import ( + "context" + "os" + "time" + + "github.com/spf13/cobra" + + "github.com/CarterPerez-dev/bomber/internal/graph" + "github.com/CarterPerez-dev/bomber/internal/parser" + "github.com/CarterPerez-dev/bomber/internal/report" + "github.com/CarterPerez-dev/bomber/internal/scanner" + "github.com/CarterPerez-dev/bomber/internal/ui" + "github.com/CarterPerez-dev/bomber/internal/vuln" + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +var vulnCmd = &cobra.Command{ + Use: "vuln [path]", + Short: "Scan for known vulnerabilities", + Args: cobra.MaximumNArgs(1), + RunE: runVuln, +} + +func runVuln(cmd *cobra.Command, args []string) error { + path := "." + if len(args) > 0 { + path = args[0] + } + + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := scanner.New(reg) + result, err := s.Scan(path) + if err != nil { + return err + } + + if len(result.Graphs) == 0 { + if formatFlag == "json" { + return report.WriteJSON(os.Stdout, result, nil, nil) + } + ui.PrintBanner() + report.PrintScanSummary(os.Stdout, result) + return nil + } + + sp := ui.NewSpinner("Querying vulnerability databases...") + if formatFlag != "json" { + sp.Start() + } + + vulnReport, err := queryVulns(cmd.Context(), result) + + if formatFlag != "json" { + sp.Stop() + } + if err != nil { + return err + } + + if formatFlag == "json" { + return report.WriteJSON(os.Stdout, result, vulnReport, nil) + } + + ui.PrintBanner() + report.PrintScanSummary(os.Stdout, result) + report.PrintVulnReport(os.Stdout, vulnReport) + return nil +} + +func queryVulns(ctx context.Context, result *types.ScanResult) (*types.VulnReport, error) { + var allPkgs []types.Package + for _, g := range result.Graphs { + allPkgs = append(allPkgs, graph.AllPackages(g)...) + } + + var cache *vuln.Cache + if !noCache { + c, err := vuln.NewCache(vuln.DefaultCachePath(), 24*time.Hour) + if err == nil { + cache = c + defer cache.Close() + } + } + + osvClient := vuln.NewOSVClient() + var clients []vuln.Client + clients = append(clients, osvClient) + + nvdKey := os.Getenv("BOMBER_NVD_API_KEY") + if nvdKey != "" { + clients = append(clients, vuln.NewNVDClient(vuln.WithNVDAPIKey(nvdKey))) + } + + vulnReport := &types.VulnReport{ + TotalPkgs: result.TotalPkgs, + DirectPkgs: result.DirectPkgs, + BySeverity: make(map[types.Severity]int), + } + + for _, client := range clients { + var uncached []types.Package + for _, pkg := range allPkgs { + if cache != nil { + cached, ok, err := cache.Get(pkg.PURL, client.Source()) + if err == nil && ok { + vulnReport.Matches = append(vulnReport.Matches, cached...) + for _, m := range cached { + vulnReport.BySeverity[m.Vulnerability.Severity]++ + } + continue + } + } + uncached = append(uncached, pkg) + } + + if len(uncached) == 0 { + continue + } + + matches, err := client.Query(ctx, uncached) + if err != nil { + continue + } + + matchesByPURL := make(map[string][]types.VulnMatch) + for _, m := range matches { + vulnReport.Matches = append(vulnReport.Matches, m) + vulnReport.BySeverity[m.Vulnerability.Severity]++ + matchesByPURL[m.Package.PURL] = append(matchesByPURL[m.Package.PURL], m) + } + + if cache != nil { + for purl, pkgMatches := range matchesByPURL { + _ = cache.Put(purl, client.Source(), pkgMatches) + } + } + } + + vulnReport.Matches = deduplicateMatches(vulnReport.Matches) + vulnReport.BySeverity = make(map[types.Severity]int) + for _, m := range vulnReport.Matches { + vulnReport.BySeverity[m.Vulnerability.Severity]++ + } + + return vulnReport, nil +} + +func deduplicateMatches(matches []types.VulnMatch) []types.VulnMatch { + seen := make(map[string]int) + var deduped []types.VulnMatch + + for _, m := range matches { + ids := make([]string, 0, 1+len(m.Vulnerability.Aliases)) + ids = append(ids, m.Vulnerability.ID) + ids = append(ids, m.Vulnerability.Aliases...) + + existingIdx := -1 + for _, id := range ids { + if idx, ok := seen[id]; ok { + existingIdx = idx + break + } + } + + if existingIdx >= 0 { + existing := deduped[existingIdx] + if m.Vulnerability.Score > existing.Vulnerability.Score || + (m.Vulnerability.FixVersion != "" && existing.Vulnerability.FixVersion == "") { + deduped[existingIdx] = m + } + continue + } + + idx := len(deduped) + for _, id := range ids { + seen[id] = idx + } + deduped = append(deduped, m) + } + + return deduped +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/config/config.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/config/config.go new file mode 100644 index 00000000..cb93ebbc --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/config/config.go @@ -0,0 +1,30 @@ +// ©AngelaMos | 2026 +// config.go + +package config + +import "time" + +const ( + ToolName = "bomber" + ToolVersion = "0.1.0" + ToolVendor = "CarterPerez-dev" + + DefaultCacheTTLHours = 24 + DefaultCachePath = ".bomber/cache.db" + + OSVBaseURL = "https://api.osv.dev" + NVDBaseURL = "https://services.nvd.nist.gov/rest/json/cves/2.0" + HTTPTimeout = 30 * time.Second + + OSVBatchSize = 1000 + OSVSourceName = "osv" + NVDSourceName = "nvd" + NVDRateWithKey = 200 * time.Millisecond + NVDRateWithoutKey = 1700 * time.Millisecond + + SPDXVersion = "SPDX-2.3" + SPDXDataLicense = "CC0-1.0" + CycloneDXFormat = "CycloneDX" + CycloneDXVersion = "1.5" +) diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/graph/graph.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/graph/graph.go new file mode 100644 index 00000000..16cba809 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/graph/graph.go @@ -0,0 +1,111 @@ +// ©AngelaMos | 2026 +// graph.go + +package graph + +import "github.com/CarterPerez-dev/bomber/pkg/types" + +func AllPackages(g *types.DependencyGraph) []types.Package { + pkgs := make([]types.Package, 0, len(g.Nodes)) + for _, pkg := range g.Nodes { + pkgs = append(pkgs, pkg) + } + return pkgs +} + +func DirectPackages(g *types.DependencyGraph) []types.Package { + var pkgs []types.Package + for _, pkg := range g.Nodes { + if pkg.Direct && pkg.PURL != g.Root.PURL { + pkgs = append(pkgs, pkg) + } + } + return pkgs +} + +func TransitivePackages(g *types.DependencyGraph) []types.Package { + var pkgs []types.Package + for _, pkg := range g.Nodes { + if !pkg.Direct && pkg.PURL != g.Root.PURL { + pkgs = append(pkgs, pkg) + } + } + return pkgs +} + +func MaxDepth(g *types.DependencyGraph) int { + maxVal := 0 + for _, pkg := range g.Nodes { + if pkg.DepthLevel > maxVal { + maxVal = pkg.DepthLevel + } + } + return maxVal +} + +func DetectCycles(g *types.DependencyGraph) [][]string { + var cycles [][]string + visited := make(map[string]bool) + inStack := make(map[string]bool) + + var dfs func(purl string, path []string) + dfs = func(purl string, path []string) { + if inStack[purl] { + for i, p := range path { + if p == purl { + cycle := make([]string, len(path)-i) + copy(cycle, path[i:]) + cycles = append(cycles, cycle) + return + } + } + return + } + if visited[purl] { + return + } + + visited[purl] = true + inStack[purl] = true + path = append(path, purl) + + for _, child := range g.Edges[purl] { + dfs(child, path) + } + + inStack[purl] = false + } + + for purl := range g.Nodes { + if !visited[purl] { + dfs(purl, nil) + } + } + + return cycles +} + +func MergeGraphs(graphs []*types.DependencyGraph) *types.DependencyGraph { + if len(graphs) == 0 { + root := types.Package{Name: "merged", PURL: "pkg:merged/root"} + return types.NewDependencyGraph(root) + } + if len(graphs) == 1 { + return graphs[0] + } + + root := types.Package{Name: "merged", PURL: "pkg:merged/root"} + merged := types.NewDependencyGraph(root) + + for _, g := range graphs { + for purl, pkg := range g.Nodes { + merged.Nodes[purl] = pkg + } + for parent, children := range g.Edges { + merged.Edges[parent] = append(merged.Edges[parent], children...) + } + merged.AddEdge(root.PURL, g.Root.PURL) + } + + return merged +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/graph/graph_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/graph/graph_test.go new file mode 100644 index 00000000..5af8e9bb --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/graph/graph_test.go @@ -0,0 +1,101 @@ +// ©AngelaMos | 2026 +// graph_test.go + +package graph + +import ( + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeTestGraph() *types.DependencyGraph { + root := types.Package{Name: "root", PURL: "pkg:test/root@1.0.0"} + g := types.NewDependencyGraph(root) + + a := types.Package{Name: "a", PURL: "pkg:test/a@1.0.0", Direct: true, DepthLevel: 1} + b := types.Package{Name: "b", PURL: "pkg:test/b@1.0.0", Direct: true, DepthLevel: 1} + c := types.Package{Name: "c", PURL: "pkg:test/c@1.0.0", Direct: false, DepthLevel: 2} + + g.AddPackage(a) + g.AddPackage(b) + g.AddPackage(c) + g.AddEdge(root.PURL, a.PURL) + g.AddEdge(root.PURL, b.PURL) + g.AddEdge(a.PURL, c.PURL) + g.AddEdge(b.PURL, c.PURL) + + return g +} + +func TestAllPackages(t *testing.T) { + g := makeTestGraph() + pkgs := AllPackages(g) + assert.Len(t, pkgs, 4) +} + +func TestDirectPackages(t *testing.T) { + g := makeTestGraph() + pkgs := DirectPackages(g) + assert.Len(t, pkgs, 2) +} + +func TestTransitivePackages(t *testing.T) { + g := makeTestGraph() + pkgs := TransitivePackages(g) + assert.Len(t, pkgs, 1) +} + +func TestMaxDepth(t *testing.T) { + g := makeTestGraph() + assert.Equal(t, 2, MaxDepth(g)) +} + +func TestDetectCyclesNone(t *testing.T) { + g := makeTestGraph() + cycles := DetectCycles(g) + assert.Empty(t, cycles) +} + +func TestDetectCyclesFound(t *testing.T) { + root := types.Package{Name: "root", PURL: "pkg:test/root@1.0.0"} + g := types.NewDependencyGraph(root) + + a := types.Package{Name: "a", PURL: "pkg:test/a@1.0.0"} + b := types.Package{Name: "b", PURL: "pkg:test/b@1.0.0"} + + g.AddPackage(a) + g.AddPackage(b) + g.AddEdge(root.PURL, a.PURL) + g.AddEdge(a.PURL, b.PURL) + g.AddEdge(b.PURL, a.PURL) + + cycles := DetectCycles(g) + require.NotEmpty(t, cycles) +} + +func TestMergeGraphs(t *testing.T) { + g1 := makeTestGraph() + + root2 := types.Package{Name: "root2", PURL: "pkg:test/root2@1.0.0"} + g2 := types.NewDependencyGraph(root2) + d := types.Package{Name: "d", PURL: "pkg:test/d@1.0.0"} + g2.AddPackage(d) + g2.AddEdge(root2.PURL, d.PURL) + + merged := MergeGraphs([]*types.DependencyGraph{g1, g2}) + assert.Len(t, AllPackages(merged), 7) +} + +func TestMergeGraphsEmpty(t *testing.T) { + merged := MergeGraphs(nil) + assert.Equal(t, "merged", merged.Root.Name) +} + +func TestMergeGraphsSingle(t *testing.T) { + g := makeTestGraph() + merged := MergeGraphs([]*types.DependencyGraph{g}) + assert.Equal(t, g, merged) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/gomod.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/gomod.go new file mode 100644 index 00000000..584a7647 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/gomod.go @@ -0,0 +1,227 @@ +// ©AngelaMos | 2026 +// gomod.go + +package parser + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/hex" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +type GoModParser struct{} + +func NewGoModParser() *GoModParser { + return &GoModParser{} +} + +func (p *GoModParser) Ecosystem() types.Ecosystem { + return types.EcosystemGo +} + +func (p *GoModParser) Detect(dir string) bool { + _, err := os.Stat(filepath.Join(dir, "go.mod")) + return err == nil +} + +func (p *GoModParser) Parse(dir string) (*types.DependencyGraph, error) { + modPath := filepath.Join(dir, "go.mod") + modFile, err := os.Open(modPath) + if err != nil { + return nil, fmt.Errorf("open go.mod: %w", err) + } + defer modFile.Close() + + var moduleName string + directDeps := make(map[string]string) + indirectDeps := make(map[string]string) + + scanner := bufio.NewScanner(modFile) + inRequire := false + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "module ") { + moduleName = strings.TrimPrefix(line, "module ") + continue + } + + if line == "require (" { + inRequire = true + continue + } + if line == ")" { + inRequire = false + continue + } + + if inRequire { + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + name := parts[0] + version := parts[1] + if strings.Contains(line, "// indirect") { + indirectDeps[name] = version + } else { + directDeps[name] = version + } + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("read go.mod: %w", err) + } + + root := types.Package{ + Name: moduleName, + Ecosystem: types.EcosystemGo, + PURL: fmt.Sprintf("pkg:golang/%s", moduleName), + Direct: true, + } + graph := types.NewDependencyGraph(root) + + checksums := parseGoSum(filepath.Join(dir, "go.sum")) + + for name, version := range directDeps { + purl := fmt.Sprintf("pkg:golang/%s@%s", name, version) + pkg := types.Package{ + Name: name, + Version: version, + Ecosystem: types.EcosystemGo, + PURL: purl, + Direct: true, + Checksums: checksums[name+"@"+version], + } + graph.AddPackage(pkg) + graph.AddEdge(root.PURL, purl) + } + + for name, version := range indirectDeps { + purl := fmt.Sprintf("pkg:golang/%s@%s", name, version) + pkg := types.Package{ + Name: name, + Version: version, + Ecosystem: types.EcosystemGo, + PURL: purl, + Direct: false, + Checksums: checksums[name+"@"+version], + } + graph.AddPackage(pkg) + } + + parseGoModGraph(dir, moduleName, graph) + computeDepthLevels(graph) + + return graph, nil +} + +func parseGoModGraph(dir, moduleName string, graph *types.DependencyGraph) { + cmd := exec.Command("go", "mod", "graph") + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + return + } + + scanner := bufio.NewScanner(bytes.NewReader(out)) + for scanner.Scan() { + parts := strings.Fields(scanner.Text()) + if len(parts) != 2 { + continue + } + parentPURL := goDepToPURL(parts[0], moduleName) + childPURL := goDepToPURL(parts[1], moduleName) + + if _, exists := graph.Nodes[childPURL]; !exists { + continue + } + if _, exists := graph.Nodes[parentPURL]; !exists { + continue + } + + graph.AddEdge(parentPURL, childPURL) + } +} + +func goDepToPURL(dep, moduleName string) string { + if !strings.Contains(dep, "@") { + return fmt.Sprintf("pkg:golang/%s", dep) + } + parts := strings.SplitN(dep, "@", 2) + return fmt.Sprintf("pkg:golang/%s@%s", parts[0], parts[1]) +} + +func computeDepthLevels(graph *types.DependencyGraph) { + depths := make(map[string]int) + depths[graph.Root.PURL] = 0 + queue := []string{graph.Root.PURL} + + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + currentDepth := depths[current] + + for _, child := range graph.Edges[current] { + if _, visited := depths[child]; !visited { + depths[child] = currentDepth + 1 + queue = append(queue, child) + } + } + } + + for purl, depth := range depths { + if pkg, ok := graph.Nodes[purl]; ok { + pkg.DepthLevel = depth + graph.Nodes[purl] = pkg + } + } +} + +func parseGoSum(path string) map[string][]types.Checksum { + checksums := make(map[string][]types.Checksum) + + f, err := os.Open(path) + if err != nil { + return checksums + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + parts := strings.Fields(scanner.Text()) + if len(parts) != 3 { + continue + } + name := parts[0] + version := strings.TrimSuffix(parts[1], "/go.mod") + hash := parts[2] + + if !strings.HasPrefix(hash, "h1:") { + continue + } + raw := strings.TrimPrefix(hash, "h1:") + decoded, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + continue + } + + key := name + "@" + version + checksums[key] = append(checksums[key], types.Checksum{ + Algorithm: "SHA-256", + Value: hex.EncodeToString(decoded), + }) + } + + return checksums +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/gomod_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/gomod_test.go new file mode 100644 index 00000000..a9663079 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/gomod_test.go @@ -0,0 +1,70 @@ +// ©AngelaMos | 2026 +// gomod_test.go + +package parser + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testdataDir(t *testing.T) string { + t.Helper() + _, filename, _, ok := runtime.Caller(0) + require.True(t, ok) + return filepath.Join(filepath.Dir(filename), "..", "..", "testdata") +} + +func TestGoModDetect(t *testing.T) { + p := NewGoModParser() + assert.True(t, p.Detect(filepath.Join(testdataDir(t), "go-project"))) + assert.False(t, p.Detect(filepath.Join(testdataDir(t), "node-project"))) + assert.False(t, p.Detect(filepath.Join(testdataDir(t), "empty-project"))) +} + +func TestGoModParse(t *testing.T) { + p := NewGoModParser() + dir := filepath.Join(testdataDir(t), "go-project") + graph, err := p.Parse(dir) + require.NoError(t, err) + + assert.Equal(t, "example.com/testproject", graph.Root.Name) + assert.Equal(t, types.EcosystemGo, graph.Root.Ecosystem) + + directCount := 0 + for _, pkg := range graph.Nodes { + if pkg.Direct && pkg.PURL != graph.Root.PURL { + directCount++ + } + } + assert.Equal(t, 2, directCount) + + _, hasCobra := graph.Nodes["pkg:golang/github.com/spf13/cobra@v1.10.2"] + assert.True(t, hasCobra) + + _, hasNet := graph.Nodes["pkg:golang/golang.org/x/net@v0.1.0"] + assert.True(t, hasNet) + + _, hasMousetrap := graph.Nodes["pkg:golang/github.com/inconshreveable/mousetrap@v1.1.0"] + assert.True(t, hasMousetrap) +} + +func TestGoModParseChecksums(t *testing.T) { + p := NewGoModParser() + dir := filepath.Join(testdataDir(t), "go-project") + graph, err := p.Parse(dir) + require.NoError(t, err) + + cobra := graph.Nodes["pkg:golang/github.com/spf13/cobra@v1.10.2"] + assert.NotEmpty(t, cobra.Checksums) +} + +func TestGoModEcosystem(t *testing.T) { + p := NewGoModParser() + assert.Equal(t, types.EcosystemGo, p.Ecosystem()) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/node.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/node.go new file mode 100644 index 00000000..bb36aa34 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/node.go @@ -0,0 +1,209 @@ +// ©AngelaMos | 2026 +// node.go + +package parser + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "gopkg.in/yaml.v3" +) + +var nodeSemverRe = regexp.MustCompile(`\d+\.\d+\.\d+`) + +type NodeParser struct{} + +func NewNodeParser() *NodeParser { + return &NodeParser{} +} + +func (p *NodeParser) Ecosystem() types.Ecosystem { + return types.EcosystemNode +} + +func (p *NodeParser) Detect(dir string) bool { + _, err := os.Stat(filepath.Join(dir, "package.json")) + return err == nil +} + +func (p *NodeParser) Parse(dir string) (*types.DependencyGraph, error) { + pkgPath := filepath.Join(dir, "package.json") + data, err := os.ReadFile(pkgPath) + if err != nil { + return nil, fmt.Errorf("read package.json: %w", err) + } + + var pkg packageJSON + if err := json.Unmarshal(data, &pkg); err != nil { + return nil, fmt.Errorf("parse package.json: %w", err) + } + + root := types.Package{ + Name: pkg.Name, + Version: pkg.Version, + Ecosystem: types.EcosystemNode, + PURL: fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(pkg.Name), pkg.Version), + Direct: true, + } + graph := types.NewDependencyGraph(root) + + directNames := make(map[string]bool) + for name := range pkg.Dependencies { + directNames[name] = true + } + for name := range pkg.DevDependencies { + directNames[name] = true + } + + lockPath := filepath.Join(dir, "pnpm-lock.yaml") + lockData, err := os.ReadFile(lockPath) + if err == nil { + parsePnpmLock(lockData, graph, root.PURL, directNames) + } else { + parseFromPackageJSON(pkg, graph, root.PURL) + } + + return graph, nil +} + +type packageJSON struct { + Name string `json:"name"` + Version string `json:"version"` + Dependencies map[string]string `json:"dependencies"` + DevDependencies map[string]string `json:"devDependencies"` +} + +type pnpmLockfile struct { + Packages map[string]pnpmPackage `yaml:"packages"` + Snapshots map[string]pnpmSnapshot `yaml:"snapshots"` +} + +type pnpmPackage struct { + Resolution struct { + Integrity string `yaml:"integrity"` + } `yaml:"resolution"` +} + +type pnpmSnapshot struct { + Dependencies map[string]string `yaml:"dependencies"` +} + +func parsePnpmLock( + data []byte, + graph *types.DependencyGraph, + rootPURL string, + directNames map[string]bool, +) { + var lock pnpmLockfile + if err := yaml.Unmarshal(data, &lock); err != nil { + return + } + + for key, pkgEntry := range lock.Packages { + name, version := splitPnpmKey(key) + if name == "" { + continue + } + + isDirect := directNames[name] + depth := 2 + if isDirect { + depth = 1 + } + + purl := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(name), version) + pkg := types.Package{ + Name: name, + Version: version, + Ecosystem: types.EcosystemNode, + PURL: purl, + Direct: isDirect, + DepthLevel: depth, + } + + if pkgEntry.Resolution.Integrity != "" { + pkg.Checksums = append(pkg.Checksums, types.Checksum{ + Algorithm: "SHA-512", + Value: pkgEntry.Resolution.Integrity, + }) + } + + graph.AddPackage(pkg) + if isDirect { + graph.AddEdge(rootPURL, purl) + } + } + + for key, snap := range lock.Snapshots { + parentName, parentVersion := splitPnpmKey(key) + parentPURL := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(parentName), parentVersion) + for depName, depVersion := range snap.Dependencies { + childPURL := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(depName), depVersion) + graph.AddEdge(parentPURL, childPURL) + } + } +} + +func parseFromPackageJSON( + pkg packageJSON, + graph *types.DependencyGraph, + rootPURL string, +) { + for name, version := range pkg.Dependencies { + cleanVersion := cleanNodeVersion(version) + purl := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(name), cleanVersion) + dep := types.Package{ + Name: name, + Version: cleanVersion, + Ecosystem: types.EcosystemNode, + PURL: purl, + Direct: true, + DepthLevel: 1, + } + graph.AddPackage(dep) + graph.AddEdge(rootPURL, purl) + } + for name, version := range pkg.DevDependencies { + cleanVersion := cleanNodeVersion(version) + purl := fmt.Sprintf("pkg:npm/%s@%s", encodePURLName(name), cleanVersion) + dep := types.Package{ + Name: name, + Version: cleanVersion, + Ecosystem: types.EcosystemNode, + PURL: purl, + Direct: true, + DepthLevel: 1, + } + graph.AddPackage(dep) + graph.AddEdge(rootPURL, purl) + } +} + +func splitPnpmKey(key string) (string, string) { + atIdx := strings.LastIndex(key, "@") + if atIdx <= 0 { + return "", "" + } + return key[:atIdx], key[atIdx+1:] +} + +func cleanNodeVersion(constraint string) string { + match := nodeSemverRe.FindString(constraint) + if match != "" { + return match + } + return strings.TrimLeft(constraint, "^~>=< ") +} + +func encodePURLName(name string) string { + if strings.HasPrefix(name, "@") { + return strings.Replace(name, "@", "%40", 1) + } + return name +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/node_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/node_test.go new file mode 100644 index 00000000..3526b68a --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/node_test.go @@ -0,0 +1,89 @@ +// ©AngelaMos | 2026 +// node_test.go + +package parser + +import ( + "path/filepath" + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNodeDetect(t *testing.T) { + p := NewNodeParser() + assert.True(t, p.Detect(filepath.Join(testdataDir(t), "node-project"))) + assert.False(t, p.Detect(filepath.Join(testdataDir(t), "go-project"))) + assert.False(t, p.Detect(filepath.Join(testdataDir(t), "empty-project"))) +} + +func TestNodeParse(t *testing.T) { + p := NewNodeParser() + dir := filepath.Join(testdataDir(t), "node-project") + graph, err := p.Parse(dir) + require.NoError(t, err) + + assert.Equal(t, "test-project", graph.Root.Name) + assert.Equal(t, types.EcosystemNode, graph.Root.Ecosystem) + + directCount := 0 + for _, pkg := range graph.Nodes { + if pkg.Direct && pkg.PURL != graph.Root.PURL { + directCount++ + } + } + assert.Equal(t, 3, directCount) + + hasExpress := false + hasLodash := false + hasTS := false + for _, pkg := range graph.Nodes { + switch pkg.Name { + case "express": + hasExpress = true + assert.Equal(t, "4.18.2", pkg.Version) + case "lodash": + hasLodash = true + assert.Equal(t, "4.17.20", pkg.Version) + case "typescript": + hasTS = true + assert.Equal(t, "5.3.3", pkg.Version) + } + } + assert.True(t, hasExpress) + assert.True(t, hasLodash) + assert.True(t, hasTS) +} + +func TestNodeParseTransitive(t *testing.T) { + p := NewNodeParser() + dir := filepath.Join(testdataDir(t), "node-project") + graph, err := p.Parse(dir) + require.NoError(t, err) + + hasSemver := false + for _, pkg := range graph.Nodes { + if pkg.Name == "semver" { + hasSemver = true + assert.False(t, pkg.Direct) + } + } + assert.True(t, hasSemver) +} + +func TestNodeEcosystem(t *testing.T) { + p := NewNodeParser() + assert.Equal(t, types.EcosystemNode, p.Ecosystem()) +} + +func TestCleanNodeVersion(t *testing.T) { + assert.Equal(t, "4.18.2", cleanNodeVersion("^4.18.2")) + assert.Equal(t, "4.18.2", cleanNodeVersion("~4.18.2")) + assert.Equal(t, "1.2.3", cleanNodeVersion(">=1.2.3")) + assert.Equal(t, "2.0.0", cleanNodeVersion("<2.0.0")) + assert.Equal(t, "1.0.0", cleanNodeVersion(">=1.0.0 <2.0.0")) + assert.Equal(t, "4.18.2", cleanNodeVersion("4.18.2")) + assert.Equal(t, "1.0.0", cleanNodeVersion("=1.0.0")) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/parser.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/parser.go new file mode 100644 index 00000000..e0ae460a --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/parser.go @@ -0,0 +1,12 @@ +// ©AngelaMos | 2026 +// parser.go + +package parser + +import "github.com/CarterPerez-dev/bomber/pkg/types" + +type DependencyParser interface { + Detect(dir string) bool + Parse(dir string) (*types.DependencyGraph, error) + Ecosystem() types.Ecosystem +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/python.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/python.go new file mode 100644 index 00000000..3ec12721 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/python.go @@ -0,0 +1,194 @@ +// ©AngelaMos | 2026 +// python.go + +package parser + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/pelletier/go-toml/v2" +) + +type PythonParser struct{} + +func NewPythonParser() *PythonParser { + return &PythonParser{} +} + +func (p *PythonParser) Ecosystem() types.Ecosystem { + return types.EcosystemPython +} + +func (p *PythonParser) Detect(dir string) bool { + _, err := os.Stat(filepath.Join(dir, "pyproject.toml")) + return err == nil +} + +func (p *PythonParser) Parse(dir string) (*types.DependencyGraph, error) { + pyPath := filepath.Join(dir, "pyproject.toml") + data, err := os.ReadFile(pyPath) + if err != nil { + return nil, fmt.Errorf("read pyproject.toml: %w", err) + } + + var proj pyprojectTOML + if err := toml.Unmarshal(data, &proj); err != nil { + return nil, fmt.Errorf("parse pyproject.toml: %w", err) + } + + root := types.Package{ + Name: proj.Project.Name, + Version: proj.Project.Version, + Ecosystem: types.EcosystemPython, + PURL: fmt.Sprintf("pkg:pypi/%s@%s", proj.Project.Name, proj.Project.Version), + Direct: true, + } + graph := types.NewDependencyGraph(root) + + directNames := make(map[string]bool) + for _, dep := range proj.Project.Dependencies { + name := extractPyPkgName(dep) + directNames[strings.ToLower(name)] = true + } + for _, groups := range proj.DependencyGroups { + for _, dep := range groups { + name := extractPyPkgName(dep) + directNames[strings.ToLower(name)] = true + } + } + + lockPath := filepath.Join(dir, "uv.lock") + lockData, err := os.ReadFile(lockPath) + if err == nil { + parseUVLock(lockData, graph, root.PURL, directNames) + } else { + parseFromPyproject(proj, graph, root.PURL) + } + + return graph, nil +} + +type pyprojectTOML struct { + Project struct { + Name string `toml:"name"` + Version string `toml:"version"` + Dependencies []string `toml:"dependencies"` + } `toml:"project"` + DependencyGroups map[string][]string `toml:"dependency-groups"` +} + +type uvLockfile struct { + Packages []uvPackage `toml:"package"` +} + +type uvPackage struct { + Name string `toml:"name"` + Version string `toml:"version"` + Source uvSource `toml:"source"` + Dependencies []uvDependency `toml:"dependencies"` +} + +type uvSource struct { + Registry string `toml:"registry"` + Virtual string `toml:"virtual"` +} + +type uvDependency struct { + Name string `toml:"name"` +} + +func parseUVLock( + data []byte, + graph *types.DependencyGraph, + rootPURL string, + directNames map[string]bool, +) { + var lock uvLockfile + if err := toml.Unmarshal(data, &lock); err != nil { + return + } + + purlMap := make(map[string]string) + + for _, pkg := range lock.Packages { + if pkg.Source.Virtual != "" { + continue + } + + normalizedName := strings.ToLower(pkg.Name) + isDirect := directNames[normalizedName] + depth := 2 + if isDirect { + depth = 1 + } + + purl := fmt.Sprintf("pkg:pypi/%s@%s", normalizedName, pkg.Version) + purlMap[normalizedName] = purl + + dep := types.Package{ + Name: pkg.Name, + Version: pkg.Version, + Ecosystem: types.EcosystemPython, + PURL: purl, + Direct: isDirect, + DepthLevel: depth, + } + graph.AddPackage(dep) + + if isDirect { + graph.AddEdge(rootPURL, purl) + } + } + + for _, pkg := range lock.Packages { + if pkg.Source.Virtual != "" { + continue + } + parentName := strings.ToLower(pkg.Name) + parentPURL := purlMap[parentName] + if parentPURL == "" { + continue + } + for _, dep := range pkg.Dependencies { + childPURL := purlMap[strings.ToLower(dep.Name)] + if childPURL != "" { + graph.AddEdge(parentPURL, childPURL) + } + } + } +} + +func parseFromPyproject( + proj pyprojectTOML, + graph *types.DependencyGraph, + rootPURL string, +) { + for _, dep := range proj.Project.Dependencies { + name := extractPyPkgName(dep) + purl := fmt.Sprintf("pkg:pypi/%s", strings.ToLower(name)) + pkg := types.Package{ + Name: name, + Ecosystem: types.EcosystemPython, + PURL: purl, + Direct: true, + DepthLevel: 1, + } + graph.AddPackage(pkg) + graph.AddEdge(rootPURL, purl) + } +} + +var pyVersionRe = regexp.MustCompile(`[><=!~;]`) + +func extractPyPkgName(spec string) string { + loc := pyVersionRe.FindStringIndex(spec) + if loc != nil { + return strings.TrimSpace(spec[:loc[0]]) + } + return strings.TrimSpace(spec) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/python_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/python_test.go new file mode 100644 index 00000000..b5c900b8 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/python_test.go @@ -0,0 +1,100 @@ +// ©AngelaMos | 2026 +// python_test.go + +package parser + +import ( + "path/filepath" + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPythonDetect(t *testing.T) { + p := NewPythonParser() + assert.True(t, p.Detect(filepath.Join(testdataDir(t), "python-project"))) + assert.False(t, p.Detect(filepath.Join(testdataDir(t), "go-project"))) + assert.False(t, p.Detect(filepath.Join(testdataDir(t), "empty-project"))) +} + +func TestPythonParse(t *testing.T) { + p := NewPythonParser() + dir := filepath.Join(testdataDir(t), "python-project") + graph, err := p.Parse(dir) + require.NoError(t, err) + + assert.Equal(t, "test-project", graph.Root.Name) + assert.Equal(t, types.EcosystemPython, graph.Root.Ecosystem) + + directCount := 0 + for _, pkg := range graph.Nodes { + if pkg.Direct && pkg.PURL != graph.Root.PURL { + directCount++ + } + } + assert.GreaterOrEqual(t, directCount, 2) + + hasRequests := false + hasPydantic := false + for _, pkg := range graph.Nodes { + switch pkg.Name { + case "requests": + hasRequests = true + assert.Equal(t, "2.31.0", pkg.Version) + assert.True(t, pkg.Direct) + case "pydantic": + hasPydantic = true + assert.Equal(t, "2.6.1", pkg.Version) + assert.True(t, pkg.Direct) + } + } + assert.True(t, hasRequests) + assert.True(t, hasPydantic) +} + +func TestPythonParseTransitive(t *testing.T) { + p := NewPythonParser() + dir := filepath.Join(testdataDir(t), "python-project") + graph, err := p.Parse(dir) + require.NoError(t, err) + + hasUrllib3 := false + hasCertifi := false + for _, pkg := range graph.Nodes { + switch pkg.Name { + case "urllib3": + hasUrllib3 = true + assert.False(t, pkg.Direct) + case "certifi": + hasCertifi = true + assert.False(t, pkg.Direct) + } + } + assert.True(t, hasUrllib3) + assert.True(t, hasCertifi) +} + +func TestPythonParseEdges(t *testing.T) { + p := NewPythonParser() + dir := filepath.Join(testdataDir(t), "python-project") + graph, err := p.Parse(dir) + require.NoError(t, err) + + requestsPURL := "pkg:pypi/requests@2.31.0" + children := graph.Edges[requestsPURL] + assert.NotEmpty(t, children) +} + +func TestPythonEcosystem(t *testing.T) { + p := NewPythonParser() + assert.Equal(t, types.EcosystemPython, p.Ecosystem()) +} + +func TestExtractPyPkgName(t *testing.T) { + assert.Equal(t, "requests", extractPyPkgName("requests>=2.31.0")) + assert.Equal(t, "pydantic", extractPyPkgName("pydantic>=2.5.0")) + assert.Equal(t, "pytest", extractPyPkgName("pytest>=8.0.0")) + assert.Equal(t, "simple", extractPyPkgName("simple")) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/registry.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/registry.go new file mode 100644 index 00000000..356f11b2 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/registry.go @@ -0,0 +1,36 @@ +// ©AngelaMos | 2026 +// registry.go + +package parser + +type Registry struct { + parsers []DependencyParser +} + +func NewRegistry() *Registry { + return &Registry{} +} + +func (r *Registry) Register(p DependencyParser) { + r.parsers = append(r.parsers, p) +} + +func (r *Registry) Detect(dir string) []DependencyParser { + var matched []DependencyParser + for _, p := range r.parsers { + if p.Detect(dir) { + matched = append(matched, p) + } + } + return matched +} + +func (r *Registry) All() []DependencyParser { + return r.parsers +} + +func RegisterAll(reg *Registry) { + reg.Register(NewGoModParser()) + reg.Register(NewNodeParser()) + reg.Register(NewPythonParser()) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/registry_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/registry_test.go new file mode 100644 index 00000000..a5ae1c45 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/parser/registry_test.go @@ -0,0 +1,48 @@ +// ©AngelaMos | 2026 +// registry_test.go + +package parser + +import ( + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type stubParser struct { + ecosystem types.Ecosystem + detected bool +} + +func (s *stubParser) Detect(string) bool { return s.detected } +func (s *stubParser) Parse(string) (*types.DependencyGraph, error) { return nil, nil } +func (s *stubParser) Ecosystem() types.Ecosystem { return s.ecosystem } + +func TestRegistryDetect(t *testing.T) { + reg := NewRegistry() + goParser := &stubParser{ecosystem: types.EcosystemGo, detected: true} + nodeParser := &stubParser{ecosystem: types.EcosystemNode, detected: false} + + reg.Register(goParser) + reg.Register(nodeParser) + + detected := reg.Detect("some/dir") + require.Len(t, detected, 1) + assert.Equal(t, types.EcosystemGo, detected[0].Ecosystem()) +} + +func TestRegistryDetectEmpty(t *testing.T) { + reg := NewRegistry() + detected := reg.Detect("some/dir") + assert.Empty(t, detected) +} + +func TestRegistryAll(t *testing.T) { + reg := NewRegistry() + reg.Register(&stubParser{ecosystem: types.EcosystemGo}) + reg.Register(&stubParser{ecosystem: types.EcosystemNode}) + + assert.Len(t, reg.All(), 2) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/engine.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/engine.go new file mode 100644 index 00000000..b2c38fa8 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/engine.go @@ -0,0 +1,70 @@ +// ©AngelaMos | 2026 +// engine.go + +package policy + +import ( + "fmt" + "time" + + "github.com/CarterPerez-dev/bomber/internal/graph" + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +func Evaluate( + p *Policy, + report *types.VulnReport, + graphs []*types.DependencyGraph, +) *types.CheckResult { + result := &types.CheckResult{Passed: true} + + if p.MaxSeverity != "" { + threshold := types.ParseSeverity(p.MaxSeverity) + for _, m := range report.Matches { + if m.Vulnerability.Severity.Rank() > threshold.Rank() { + v := m.Vulnerability + result.Violations = append(result.Violations, types.PolicyViolation{ + Rule: "max_severity", + Message: fmt.Sprintf("%s has severity %s (max allowed: %s)", v.ID, v.Severity, p.MaxSeverity), + Package: m.Package, + Vuln: &v, + }) + result.Passed = false + } + } + } + + if p.MaxAgeDays > 0 { + cutoff := time.Now().AddDate(0, 0, -p.MaxAgeDays) + for _, m := range report.Matches { + if !m.Vulnerability.Published.IsZero() && m.Vulnerability.Published.Before(cutoff) { + v := m.Vulnerability + result.Violations = append(result.Violations, types.PolicyViolation{ + Rule: "max_age_days", + Message: fmt.Sprintf( + "%s published %s (older than %d days)", + v.ID, v.Published.Format("2006-01-02"), p.MaxAgeDays, + ), + Package: m.Package, + Vuln: &v, + }) + result.Passed = false + } + } + } + + if p.MaxDepth > 0 { + for _, g := range graphs { + depth := graph.MaxDepth(g) + if depth > p.MaxDepth { + result.Violations = append(result.Violations, types.PolicyViolation{ + Rule: "max_depth", + Message: fmt.Sprintf("dependency depth %d exceeds maximum %d", depth, p.MaxDepth), + }) + result.Passed = false + } + } + } + + return result +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/engine_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/engine_test.go new file mode 100644 index 00000000..b34fa671 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/engine_test.go @@ -0,0 +1,144 @@ +// ©AngelaMos | 2026 +// engine_test.go + +package policy + +import ( + "testing" + "time" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEvaluatePassesClean(t *testing.T) { + p := &Policy{MaxSeverity: "high"} + report := &types.VulnReport{ + Matches: []types.VulnMatch{ + {Vulnerability: types.Vulnerability{Severity: types.SeverityMedium, ID: "CVE-TEST"}}, + }, + } + + result := Evaluate(p, report, nil) + assert.True(t, result.Passed) + assert.Empty(t, result.Violations) +} + +func TestEvaluateFailsCritical(t *testing.T) { + p := &Policy{MaxSeverity: "medium"} + report := &types.VulnReport{ + Matches: []types.VulnMatch{ + {Vulnerability: types.Vulnerability{Severity: types.SeverityCritical, ID: "CVE-2024-0001"}}, + }, + } + + result := Evaluate(p, report, nil) + assert.False(t, result.Passed) + require.NotEmpty(t, result.Violations) + assert.Equal(t, "max_severity", result.Violations[0].Rule) +} + +func TestEvaluateMaxDepth(t *testing.T) { + p := &Policy{MaxDepth: 3} + + root := types.Package{Name: "root", PURL: "pkg:test/root@1.0.0"} + g := types.NewDependencyGraph(root) + deep := types.Package{Name: "deep", PURL: "pkg:test/deep@1.0.0", DepthLevel: 5} + g.AddPackage(deep) + + result := Evaluate(p, &types.VulnReport{}, []*types.DependencyGraph{g}) + assert.False(t, result.Passed) + require.NotEmpty(t, result.Violations) + assert.Equal(t, "max_depth", result.Violations[0].Rule) +} + +func TestEvaluateMaxDepthPasses(t *testing.T) { + p := &Policy{MaxDepth: 10} + + root := types.Package{Name: "root", PURL: "pkg:test/root@1.0.0"} + g := types.NewDependencyGraph(root) + shallow := types.Package{Name: "shallow", PURL: "pkg:test/shallow@1.0.0", DepthLevel: 2} + g.AddPackage(shallow) + + result := Evaluate(p, &types.VulnReport{}, []*types.DependencyGraph{g}) + assert.True(t, result.Passed) +} + +func TestLoadPolicyYAML(t *testing.T) { + yml := ` +max_severity: medium +max_depth: 5 +max_age_days: 365 +` + p, err := ParsePolicy([]byte(yml)) + require.NoError(t, err) + assert.Equal(t, "medium", p.MaxSeverity) + assert.Equal(t, 5, p.MaxDepth) + assert.Equal(t, 365, p.MaxAgeDays) +} + +func TestEvaluateMaxAgeFails(t *testing.T) { + p := &Policy{MaxAgeDays: 90} + report := &types.VulnReport{ + Matches: []types.VulnMatch{ + { + Vulnerability: types.Vulnerability{ + ID: "CVE-2023-0001", + Published: time.Now().AddDate(0, 0, -180), + }, + }, + }, + } + + result := Evaluate(p, report, nil) + assert.False(t, result.Passed) + require.NotEmpty(t, result.Violations) + assert.Equal(t, "max_age_days", result.Violations[0].Rule) +} + +func TestEvaluateMaxAgePasses(t *testing.T) { + p := &Policy{MaxAgeDays: 90} + report := &types.VulnReport{ + Matches: []types.VulnMatch{ + { + Vulnerability: types.Vulnerability{ + ID: "CVE-2024-0001", + Published: time.Now().AddDate(0, 0, -30), + }, + }, + }, + } + + result := Evaluate(p, report, nil) + assert.True(t, result.Passed) + assert.Empty(t, result.Violations) +} + +func TestEvaluateMaxAgeSkipsZeroPublished(t *testing.T) { + p := &Policy{MaxAgeDays: 90} + report := &types.VulnReport{ + Matches: []types.VulnMatch{ + { + Vulnerability: types.Vulnerability{ + ID: "CVE-2024-0001", + }, + }, + }, + } + + result := Evaluate(p, report, nil) + assert.True(t, result.Passed) +} + +func TestEvaluateEmptyPolicy(t *testing.T) { + p := &Policy{} + report := &types.VulnReport{ + Matches: []types.VulnMatch{ + {Vulnerability: types.Vulnerability{Severity: types.SeverityCritical}}, + }, + } + + result := Evaluate(p, report, nil) + assert.True(t, result.Passed) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/rules.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/rules.go new file mode 100644 index 00000000..3365ad34 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/policy/rules.go @@ -0,0 +1,33 @@ +// ©AngelaMos | 2026 +// rules.go + +package policy + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type Policy struct { + MaxSeverity string `yaml:"max_severity"` + MaxAgeDays int `yaml:"max_age_days"` + MaxDepth int `yaml:"max_depth"` +} + +func ParsePolicy(data []byte) (*Policy, error) { + var p Policy + if err := yaml.Unmarshal(data, &p); err != nil { + return nil, fmt.Errorf("parse policy: %w", err) + } + return &p, nil +} + +func LoadPolicy(path string) (*Policy, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read policy file: %w", err) + } + return ParsePolicy(data) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/report/json.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/report/json.go new file mode 100644 index 00000000..09daec16 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/report/json.go @@ -0,0 +1,34 @@ +// ©AngelaMos | 2026 +// json.go + +package report + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +type JSONReport struct { + Scan *types.ScanResult `json:"scan,omitempty"` + Vulns *types.VulnReport `json:"vulnerabilities,omitempty"` + Policy *types.CheckResult `json:"policy,omitempty"` +} + +func WriteJSON(w io.Writer, scan *types.ScanResult, vulns *types.VulnReport, policy *types.CheckResult) error { + report := JSONReport{ + Scan: scan, + Vulns: vulns, + Policy: policy, + } + + data, err := json.MarshalIndent(report, "", " ") + if err != nil { + return fmt.Errorf("marshal json report: %w", err) + } + + _, err = w.Write(data) + return err +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/report/terminal.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/report/terminal.go new file mode 100644 index 00000000..2b10351a --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/report/terminal.go @@ -0,0 +1,120 @@ +// ©AngelaMos | 2026 +// terminal.go + +package report + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/CarterPerez-dev/bomber/internal/graph" + "github.com/CarterPerez-dev/bomber/internal/ui" + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +func PrintScanSummary(w io.Writer, result *types.ScanResult) { + fmt.Fprintf(w, " %s Scanned %d packages (%d direct", + ui.Check, result.TotalPkgs, result.DirectPkgs) + + transitive := result.TotalPkgs - result.DirectPkgs + if transitive > 0 { + fmt.Fprintf(w, ", %d transitive", transitive) + } + fmt.Fprintln(w, ")") + + if len(result.Ecosystems) > 0 { + names := make([]string, len(result.Ecosystems)) + for i, e := range result.Ecosystems { + names[i] = e.String() + } + fmt.Fprintf(w, " %s Ecosystems: %s\n", ui.Bullet, strings.Join(names, ", ")) + } + + for _, g := range result.Graphs { + cycles := graph.DetectCycles(g) + if len(cycles) > 0 { + fmt.Fprintf(w, " %s Circular dependencies detected in %s\n", + ui.Warning, g.Root.Name) + } + } + + fmt.Fprintln(w) +} + +func PrintVulnReport(w io.Writer, report *types.VulnReport) { + if len(report.Matches) == 0 { + fmt.Fprintf(w, " %s No vulnerabilities found\n\n", ui.Green(ui.Check)) + return + } + + bySev := map[types.Severity][]types.VulnMatch{} + for _, m := range report.Matches { + bySev[m.Vulnerability.Severity] = append(bySev[m.Vulnerability.Severity], m) + } + + order := []types.Severity{ + types.SeverityCritical, + types.SeverityHigh, + types.SeverityMedium, + types.SeverityLow, + } + + for _, sev := range order { + matches := bySev[sev] + if len(matches) == 0 { + continue + } + + sort.Slice(matches, func(i, j int) bool { + return matches[i].Vulnerability.Score > matches[j].Vulnerability.Score + }) + + header := fmt.Sprintf("%s (%d)", sev, len(matches)) + switch sev { + case types.SeverityCritical: + fmt.Fprintf(w, " %s\n", ui.Red(header)) + case types.SeverityHigh: + fmt.Fprintf(w, " %s\n", ui.Yellow(header)) + case types.SeverityMedium: + fmt.Fprintf(w, " %s\n", ui.Cyan(header)) + default: + fmt.Fprintf(w, " %s\n", ui.Dim(header)) + } + + for _, m := range matches { + v := m.Vulnerability + fmt.Fprintf(w, " %s\n", m.Package.PURL) + fmt.Fprintf(w, " %s", v.ID) + if v.Summary != "" { + summary := v.Summary + if len(summary) > 60 { + summary = summary[:57] + "..." + } + fmt.Fprintf(w, " %s %s", ui.Arrow, summary) + } + if v.Score > 0 { + fmt.Fprintf(w, " (CVSS %.1f)", v.Score) + } + fmt.Fprintln(w) + if v.FixVersion != "" { + fmt.Fprintf(w, " Fix: upgrade to %s\n", v.FixVersion) + } + fmt.Fprintln(w) + } + } +} + +func PrintCheckResult(w io.Writer, result *types.CheckResult) { + if result.Passed { + fmt.Fprintf(w, " %s Policy: %s\n\n", ui.Check, ui.Green("PASS")) + return + } + + fmt.Fprintf(w, " %s Policy: %s\n\n", ui.Cross, ui.Red("FAIL")) + for _, v := range result.Violations { + fmt.Fprintf(w, " %s [%s] %s\n", ui.Cross, v.Rule, v.Message) + } + fmt.Fprintln(w) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/cyclonedx.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/cyclonedx.go new file mode 100644 index 00000000..b347e7e0 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/cyclonedx.go @@ -0,0 +1,130 @@ +// ©AngelaMos | 2026 +// cyclonedx.go + +package sbom + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/CarterPerez-dev/bomber/internal/config" + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/google/uuid" +) + +type CycloneDXGenerator struct{} + +func NewCycloneDXGenerator() *CycloneDXGenerator { + return &CycloneDXGenerator{} +} + +func (g *CycloneDXGenerator) Generate(graphs []*types.DependencyGraph) ([]byte, error) { + doc := cdxDocument{ + BOMFormat: config.CycloneDXFormat, + SpecVersion: config.CycloneDXVersion, + Version: 1, + SerialNum: fmt.Sprintf("urn:uuid:%s", uuid.New().String()), + Metadata: cdxMetadata{ + Timestamp: time.Now().UTC().Format(time.RFC3339), + Tools: []cdxTool{ + { + Vendor: config.ToolVendor, + Name: config.ToolName, + Version: config.ToolVersion, + }, + }, + }, + Components: []cdxComponent{}, + Dependencies: []cdxDependency{}, + } + + for _, graph := range graphs { + for _, pkg := range graph.Nodes { + if pkg.PURL == graph.Root.PURL { + continue + } + + comp := cdxComponent{ + Type: "library", + Name: pkg.Name, + Version: pkg.Version, + PURL: pkg.PURL, + BOMRef: pkg.PURL, + } + + for _, cs := range pkg.Checksums { + comp.Hashes = append(comp.Hashes, cdxHash{ + Alg: mapCDXAlgo(cs.Algorithm), + Content: cs.Value, + }) + } + + doc.Components = append(doc.Components, comp) + } + + for parentPURL, children := range graph.Edges { + dep := cdxDependency{ + Ref: parentPURL, + DependsOn: make([]string, len(children)), + } + copy(dep.DependsOn, children) + doc.Dependencies = append(doc.Dependencies, dep) + } + } + + return json.MarshalIndent(doc, "", " ") +} + +type cdxDocument struct { + BOMFormat string `json:"bomFormat"` + SpecVersion string `json:"specVersion"` + Version int `json:"version"` + SerialNum string `json:"serialNumber"` + Metadata cdxMetadata `json:"metadata"` + Components []cdxComponent `json:"components"` + Dependencies []cdxDependency `json:"dependencies"` +} + +type cdxMetadata struct { + Timestamp string `json:"timestamp"` + Tools []cdxTool `json:"tools"` +} + +type cdxTool struct { + Vendor string `json:"vendor"` + Name string `json:"name"` + Version string `json:"version"` +} + +type cdxComponent struct { + Type string `json:"type"` + Name string `json:"name"` + Version string `json:"version"` + PURL string `json:"purl"` + BOMRef string `json:"bom-ref"` + Hashes []cdxHash `json:"hashes,omitempty"` +} + +type cdxHash struct { + Alg string `json:"alg"` + Content string `json:"content"` +} + +type cdxDependency struct { + Ref string `json:"ref"` + DependsOn []string `json:"dependsOn"` +} + +func mapCDXAlgo(algo string) string { + switch algo { + case "SHA-256", "SHA256": + return "SHA-256" + case "SHA-512", "SHA512": + return "SHA-512" + case "SHA-1", "SHA1": + return "SHA-1" + default: + return algo + } +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/cyclonedx_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/cyclonedx_test.go new file mode 100644 index 00000000..53fc7ba6 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/cyclonedx_test.go @@ -0,0 +1,73 @@ +// ©AngelaMos | 2026 +// cyclonedx_test.go + +package sbom + +import ( + "encoding/json" + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCycloneDXGenerate(t *testing.T) { + g := makeTestGraph() + gen := NewCycloneDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + + var doc map[string]any + require.NoError(t, json.Unmarshal(data, &doc)) + + assert.Equal(t, "CycloneDX", doc["bomFormat"]) + assert.Equal(t, "1.5", doc["specVersion"]) + + components, ok := doc["components"].([]any) + require.True(t, ok) + assert.NotEmpty(t, components) + + deps, ok := doc["dependencies"].([]any) + require.True(t, ok) + assert.NotEmpty(t, deps) +} + +func TestCycloneDXValidJSON(t *testing.T) { + g := makeTestGraph() + gen := NewCycloneDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + assert.True(t, json.Valid(data)) +} + +func TestCycloneDXHasSerialNumber(t *testing.T) { + g := makeTestGraph() + gen := NewCycloneDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + + var doc cdxDocument + require.NoError(t, json.Unmarshal(data, &doc)) + + assert.Contains(t, doc.SerialNum, "urn:uuid:") +} + +func TestCycloneDXHasPURL(t *testing.T) { + g := makeTestGraph() + gen := NewCycloneDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + + var doc cdxDocument + require.NoError(t, json.Unmarshal(data, &doc)) + + hasPURL := false + for _, comp := range doc.Components { + if comp.PURL != "" { + hasPURL = true + break + } + } + assert.True(t, hasPURL) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/spdx.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/spdx.go new file mode 100644 index 00000000..c13e3b10 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/spdx.go @@ -0,0 +1,167 @@ +// ©AngelaMos | 2026 +// spdx.go + +package sbom + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/CarterPerez-dev/bomber/internal/config" + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +type SPDXGenerator struct{} + +func NewSPDXGenerator() *SPDXGenerator { + return &SPDXGenerator{} +} + +func (g *SPDXGenerator) Generate(graphs []*types.DependencyGraph) ([]byte, error) { + now := time.Now().UTC().Format(time.RFC3339) + docName := "bomber-sbom" + if len(graphs) > 0 { + docName = graphs[0].Root.Name + } + + nsHash := fmt.Sprintf("%x", sha256.Sum256([]byte(docName+now))) + namespace := fmt.Sprintf("https://spdx.org/spdxdocs/%s-%s", docName, nsHash[:16]) + + doc := spdxDocument{ + SPDXVersion: config.SPDXVersion, + DataLicense: config.SPDXDataLicense, + SPDXID: "SPDXRef-DOCUMENT", + Name: docName, + DocumentNamespace: namespace, + CreationInfo: spdxCreationInfo{ + Created: now, + Creators: []string{ + fmt.Sprintf("Tool: %s-%s", config.ToolName, config.ToolVersion), + }, + }, + Packages: []spdxPackage{}, + Relationships: []spdxRelationship{}, + } + + for _, graph := range graphs { + rootRef := sanitizeSPDXID(graph.Root.PURL) + + doc.Relationships = append(doc.Relationships, spdxRelationship{ + Element: "SPDXRef-DOCUMENT", + Type: "DESCRIBES", + Related: rootRef, + }) + + for _, pkg := range graph.Nodes { + spdxPkg := spdxPackage{ + SPDXID: sanitizeSPDXID(pkg.PURL), + Name: pkg.Name, + VersionInfo: pkg.Version, + DownloadLocation: "NOASSERTION", + FilesAnalyzed: false, + Supplier: "NOASSERTION", + ExternalRefs: []spdxExternalRef{ + { + Category: "PACKAGE-MANAGER", + Type: "purl", + Locator: pkg.PURL, + }, + }, + } + + for _, cs := range pkg.Checksums { + spdxPkg.Checksums = append(spdxPkg.Checksums, spdxChecksum{ + Algorithm: mapChecksumAlgo(cs.Algorithm), + Value: cs.Value, + }) + } + + doc.Packages = append(doc.Packages, spdxPkg) + } + + for parentPURL, children := range graph.Edges { + parentRef := sanitizeSPDXID(parentPURL) + for _, childPURL := range children { + childRef := sanitizeSPDXID(childPURL) + doc.Relationships = append(doc.Relationships, spdxRelationship{ + Element: parentRef, + Type: "DEPENDS_ON", + Related: childRef, + }) + } + } + } + + return json.MarshalIndent(doc, "", " ") +} + +type spdxDocument struct { + SPDXVersion string `json:"spdxVersion"` + DataLicense string `json:"dataLicense"` + SPDXID string `json:"SPDXID"` + Name string `json:"name"` + DocumentNamespace string `json:"documentNamespace"` + CreationInfo spdxCreationInfo `json:"creationInfo"` + Packages []spdxPackage `json:"packages"` + Relationships []spdxRelationship `json:"relationships"` +} + +type spdxCreationInfo struct { + Created string `json:"created"` + Creators []string `json:"creators"` +} + +type spdxPackage struct { + SPDXID string `json:"SPDXID"` + Name string `json:"name"` + VersionInfo string `json:"versionInfo"` + DownloadLocation string `json:"downloadLocation"` + FilesAnalyzed bool `json:"filesAnalyzed"` + Supplier string `json:"supplier"` + Checksums []spdxChecksum `json:"checksums,omitempty"` + ExternalRefs []spdxExternalRef `json:"externalRefs"` +} + +type spdxChecksum struct { + Algorithm string `json:"algorithm"` + Value string `json:"checksumValue"` +} + +type spdxExternalRef struct { + Category string `json:"referenceCategory"` + Type string `json:"referenceType"` + Locator string `json:"referenceLocator"` +} + +type spdxRelationship struct { + Element string `json:"spdxElementId"` + Type string `json:"relationshipType"` + Related string `json:"relatedSpdxElement"` +} + +func sanitizeSPDXID(purl string) string { + r := strings.NewReplacer( + "/", "-", + "@", "-", + ":", "-", + ".", "-", + "%", "-", + ) + return "SPDXRef-" + r.Replace(purl) +} + +func mapChecksumAlgo(algo string) string { + switch strings.ToUpper(algo) { + case "SHA-256", "SHA256": + return "SHA256" + case "SHA-512", "SHA512": + return "SHA512" + case "SHA-1", "SHA1": + return "SHA1" + default: + return algo + } +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/spdx_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/spdx_test.go new file mode 100644 index 00000000..70d474c3 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/sbom/spdx_test.go @@ -0,0 +1,102 @@ +// ©AngelaMos | 2026 +// spdx_test.go + +package sbom + +import ( + "encoding/json" + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeTestGraph() *types.DependencyGraph { + root := types.Package{ + Name: "test-project", Version: "1.0.0", + Ecosystem: types.EcosystemGo, + PURL: "pkg:golang/test-project@1.0.0", + Direct: true, + } + g := types.NewDependencyGraph(root) + + dep := types.Package{ + Name: "github.com/example/lib", Version: "v2.0.0", + Ecosystem: types.EcosystemGo, + PURL: "pkg:golang/github.com/example/lib@v2.0.0", + Direct: true, DepthLevel: 1, + Checksums: []types.Checksum{{Algorithm: "SHA-256", Value: "abc123"}}, + } + g.AddPackage(dep) + g.AddEdge(root.PURL, dep.PURL) + + return g +} + +func TestSPDXGenerate(t *testing.T) { + g := makeTestGraph() + gen := NewSPDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + + var doc map[string]any + require.NoError(t, json.Unmarshal(data, &doc)) + + assert.Equal(t, "SPDX-2.3", doc["spdxVersion"]) + assert.Equal(t, "CC0-1.0", doc["dataLicense"]) + + packages, ok := doc["packages"].([]any) + require.True(t, ok) + assert.GreaterOrEqual(t, len(packages), 2) + + relationships, ok := doc["relationships"].([]any) + require.True(t, ok) + assert.NotEmpty(t, relationships) +} + +func TestSPDXValidJSON(t *testing.T) { + g := makeTestGraph() + gen := NewSPDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + assert.True(t, json.Valid(data)) +} + +func TestSPDXHasDescribes(t *testing.T) { + g := makeTestGraph() + gen := NewSPDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + + var doc spdxDocument + require.NoError(t, json.Unmarshal(data, &doc)) + + hasDescribes := false + for _, rel := range doc.Relationships { + if rel.Type == "DESCRIBES" { + hasDescribes = true + break + } + } + assert.True(t, hasDescribes) +} + +func TestSPDXHasDependsOn(t *testing.T) { + g := makeTestGraph() + gen := NewSPDXGenerator() + data, err := gen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + + var doc spdxDocument + require.NoError(t, json.Unmarshal(data, &doc)) + + hasDependsOn := false + for _, rel := range doc.Relationships { + if rel.Type == "DEPENDS_ON" { + hasDependsOn = true + break + } + } + assert.True(t, hasDependsOn) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/integration_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/integration_test.go new file mode 100644 index 00000000..af728d3b --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/integration_test.go @@ -0,0 +1,101 @@ +// ©AngelaMos | 2026 +// integration_test.go + +package scanner + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/CarterPerez-dev/bomber/internal/graph" + "github.com/CarterPerez-dev/bomber/internal/parser" + "github.com/CarterPerez-dev/bomber/internal/sbom" + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFullPipelineGoProject(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "go-project")) + require.NoError(t, err) + require.Len(t, result.Graphs, 1) + + g := result.Graphs[0] + assert.Greater(t, len(graph.AllPackages(g)), 1) + assert.Empty(t, graph.DetectCycles(g)) + + spdxGen := sbom.NewSPDXGenerator() + spdxData, err := spdxGen.Generate(result.Graphs) + require.NoError(t, err) + assert.True(t, json.Valid(spdxData)) + + cdxGen := sbom.NewCycloneDXGenerator() + cdxData, err := cdxGen.Generate(result.Graphs) + require.NoError(t, err) + assert.True(t, json.Valid(cdxData)) +} + +func TestFullPipelineMonorepo(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "monorepo")) + require.NoError(t, err) + assert.GreaterOrEqual(t, len(result.Graphs), 2) + assert.GreaterOrEqual(t, len(result.Ecosystems), 2) +} + +func TestFullPipelinePythonProject(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "python-project")) + require.NoError(t, err) + require.Len(t, result.Graphs, 1) + + g := result.Graphs[0] + all := graph.AllPackages(g) + assert.Greater(t, len(all), 1) + + spdxGen := sbom.NewSPDXGenerator() + spdxData, err := spdxGen.Generate(result.Graphs) + require.NoError(t, err) + assert.True(t, json.Valid(spdxData)) +} + +func TestFullPipelineAllEcosystems(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + ecosystems := []string{"go-project", "node-project", "python-project"} + + for _, eco := range ecosystems { + t.Run(eco, func(t *testing.T) { + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), eco)) + require.NoError(t, err) + require.NotEmpty(t, result.Graphs) + assert.Greater(t, result.TotalPkgs, 0) + assert.Greater(t, result.DirectPkgs, 0) + + for _, g := range result.Graphs { + spdxGen := sbom.NewSPDXGenerator() + spdxData, err := spdxGen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + assert.True(t, json.Valid(spdxData)) + + cdxGen := sbom.NewCycloneDXGenerator() + cdxData, err := cdxGen.Generate([]*types.DependencyGraph{g}) + require.NoError(t, err) + assert.True(t, json.Valid(cdxData)) + } + }) + } +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/scanner.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/scanner.go new file mode 100644 index 00000000..a89e4467 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/scanner.go @@ -0,0 +1,86 @@ +// ©AngelaMos | 2026 +// scanner.go + +package scanner + +import ( + "os" + "path/filepath" + + "github.com/CarterPerez-dev/bomber/internal/graph" + "github.com/CarterPerez-dev/bomber/internal/parser" + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +var skipDirs = map[string]bool{ + "node_modules": true, + ".git": true, + "vendor": true, + "__pycache__": true, + ".venv": true, + "dist": true, + "build": true, + ".tox": true, + "target": true, +} + +type Scanner struct { + registry *parser.Registry +} + +func New(registry *parser.Registry) *Scanner { + return &Scanner{registry: registry} +} + +func (s *Scanner) Scan(dir string) (*types.ScanResult, error) { + result := &types.ScanResult{} + ecosystemSet := make(map[types.Ecosystem]bool) + + dirs := discoverDirs(dir) + + for _, d := range dirs { + matched := s.registry.Detect(d) + for _, p := range matched { + g, err := p.Parse(d) + if err != nil { + continue + } + result.Graphs = append(result.Graphs, g) + ecosystemSet[p.Ecosystem()] = true + } + } + + for _, g := range result.Graphs { + all := graph.AllPackages(g) + result.TotalPkgs += len(all) + result.DirectPkgs += len(graph.DirectPackages(g)) + } + + for eco := range ecosystemSet { + result.Ecosystems = append(result.Ecosystems, eco) + } + + return result, nil +} + +func discoverDirs(root string) []string { + dirs := []string{root} + + entries, err := os.ReadDir(root) + if err != nil { + return dirs + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + if skipDirs[entry.Name()] { + continue + } + subDir := filepath.Join(root, entry.Name()) + dirs = append(dirs, discoverDirs(subDir)...) + } + + return dirs +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/scanner_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/scanner_test.go new file mode 100644 index 00000000..f4c52489 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/scanner/scanner_test.go @@ -0,0 +1,81 @@ +// ©AngelaMos | 2026 +// scanner_test.go + +package scanner + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/CarterPerez-dev/bomber/internal/parser" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testdataDir(t *testing.T) string { + t.Helper() + _, filename, _, ok := runtime.Caller(0) + require.True(t, ok) + return filepath.Join(filepath.Dir(filename), "..", "..", "testdata") +} + +func TestScanGoProject(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "go-project")) + require.NoError(t, err) + + assert.Len(t, result.Graphs, 1) + assert.Greater(t, result.TotalPkgs, 0) +} + +func TestScanNodeProject(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "node-project")) + require.NoError(t, err) + + assert.Len(t, result.Graphs, 1) + assert.Greater(t, result.TotalPkgs, 0) +} + +func TestScanPythonProject(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "python-project")) + require.NoError(t, err) + + assert.Len(t, result.Graphs, 1) + assert.Greater(t, result.TotalPkgs, 0) +} + +func TestScanMonorepo(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "monorepo")) + require.NoError(t, err) + + assert.GreaterOrEqual(t, len(result.Graphs), 2) + assert.GreaterOrEqual(t, len(result.Ecosystems), 2) +} + +func TestScanEmptyProject(t *testing.T) { + reg := parser.NewRegistry() + parser.RegisterAll(reg) + + s := New(reg) + result, err := s.Scan(filepath.Join(testdataDir(t), "empty-project")) + require.NoError(t, err) + + assert.Empty(t, result.Graphs) + assert.Equal(t, 0, result.TotalPkgs) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/banner.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/banner.go new file mode 100644 index 00000000..8605b429 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/banner.go @@ -0,0 +1,18 @@ +// ©AngelaMos | 2026 +// banner.go + +package ui + +import "fmt" + +func PrintBanner() { + fmt.Printf("\n %s %s\n\n", Bold("bomber"), Dim("SBOM generator & vulnerability matcher")) +} + +func PrintBannerWithArt() { + fmt.Println() + fmt.Printf(" %s\n", Red(" ██▄ ▄▀▄ █▄ ▄█ ██▄ ██▀ █▀▄")) + fmt.Printf(" %s\n", Cyan(" █▄█ ▀▄▀ █ ▀ █ █▄█ █▄▄ █▀▄")) + fmt.Println() + fmt.Printf(" %s\n\n", Dim("SBOM generator & vulnerability matcher")) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/color.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/color.go new file mode 100644 index 00000000..452e0d82 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/color.go @@ -0,0 +1,15 @@ +// ©AngelaMos | 2026 +// color.go + +package ui + +import "github.com/fatih/color" + +var ( + Red = color.New(color.FgRed).SprintFunc() + Green = color.New(color.FgGreen).SprintFunc() + Yellow = color.New(color.FgYellow).SprintFunc() + Cyan = color.New(color.FgCyan).SprintFunc() + Bold = color.New(color.Bold).SprintFunc() + Dim = color.New(color.Faint).SprintFunc() +) diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/spinner.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/spinner.go new file mode 100644 index 00000000..9cada031 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/spinner.go @@ -0,0 +1,51 @@ +// ©AngelaMos | 2026 +// spinner.go + +package ui + +import ( + "fmt" + "sync" + "time" +) + +var spinChars = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + +type Spinner struct { + message string + stop chan struct{} + done sync.WaitGroup +} + +func NewSpinner(message string) *Spinner { + return &Spinner{ + message: message, + stop: make(chan struct{}), + } +} + +func (s *Spinner) Start() { + s.done.Add(1) + go func() { + defer s.done.Done() + i := 0 + ticker := time.NewTicker(80 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-s.stop: + fmt.Print("\r\033[K") + return + case <-ticker.C: + fmt.Printf("\r %s %s", Cyan(spinChars[i%len(spinChars)]), s.message) + i++ + } + } + }() +} + +func (s *Spinner) Stop() { + close(s.stop) + s.done.Wait() +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/symbol.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/symbol.go new file mode 100644 index 00000000..0a7461e0 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/ui/symbol.go @@ -0,0 +1,13 @@ +// ©AngelaMos | 2026 +// symbol.go + +package ui + +const ( + Check = "✓" + Cross = "✗" + Warning = "⚠" + Arrow = "→" + Bullet = "●" + Shield = "🛡" +) diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cache.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cache.go new file mode 100644 index 00000000..6a38a8b6 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cache.go @@ -0,0 +1,102 @@ +// ©AngelaMos | 2026 +// cache.go + +package vuln + +import ( + "database/sql" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + _ "modernc.org/sqlite" + + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +type Cache struct { + db *sql.DB + ttl time.Duration +} + +func NewCache(dbPath string, ttl time.Duration) (*Cache, error) { + dir := filepath.Dir(dbPath) + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("create cache dir: %w", err) + } + + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return nil, fmt.Errorf("open cache db: %w", err) + } + + createSQL := `CREATE TABLE IF NOT EXISTS vuln_cache ( + purl TEXT NOT NULL, + source TEXT NOT NULL, + data BLOB NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (purl, source) + )` + if _, err := db.Exec(createSQL); err != nil { + db.Close() + return nil, fmt.Errorf("create cache table: %w", err) + } + + return &Cache{db: db, ttl: ttl}, nil +} + +func (c *Cache) Put(purl, source string, matches []types.VulnMatch) error { + data, err := json.Marshal(matches) + if err != nil { + return fmt.Errorf("marshal cache data: %w", err) + } + + upsertSQL := `INSERT OR REPLACE INTO vuln_cache (purl, source, data, created_at) + VALUES (?, ?, ?, ?)` + _, err = c.db.Exec(upsertSQL, purl, source, data, time.Now().UnixMilli()) + if err != nil { + return fmt.Errorf("insert cache: %w", err) + } + + return nil +} + +func (c *Cache) Get(purl, source string) ([]types.VulnMatch, bool, error) { + querySQL := `SELECT data, created_at FROM vuln_cache WHERE purl = ? AND source = ?` + row := c.db.QueryRow(querySQL, purl, source) + + var data []byte + var createdAt int64 + if err := row.Scan(&data, &createdAt); err != nil { + if err == sql.ErrNoRows { + return nil, false, nil + } + return nil, false, fmt.Errorf("query cache: %w", err) + } + + created := time.UnixMilli(createdAt) + if time.Since(created) > c.ttl { + return nil, false, nil + } + + var matches []types.VulnMatch + if err := json.Unmarshal(data, &matches); err != nil { + return nil, false, fmt.Errorf("unmarshal cache data: %w", err) + } + + return matches, true, nil +} + +func (c *Cache) Close() error { + return c.db.Close() +} + +func DefaultCachePath() string { + home, err := os.UserHomeDir() + if err != nil { + return ".bomber/cache.db" + } + return filepath.Join(home, ".bomber", "cache.db") +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cache_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cache_test.go new file mode 100644 index 00000000..154e298a --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cache_test.go @@ -0,0 +1,90 @@ +// ©AngelaMos | 2026 +// cache_test.go + +package vuln + +import ( + "path/filepath" + "testing" + "time" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCachePutGet(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test.db") + cache, err := NewCache(dbPath, 24*time.Hour) + require.NoError(t, err) + defer cache.Close() + + matches := []types.VulnMatch{ + { + Package: types.Package{PURL: "pkg:golang/example@v1.0.0"}, + Vulnerability: types.Vulnerability{ID: "CVE-2024-1234", Source: "osv"}, + }, + } + + require.NoError(t, cache.Put("pkg:golang/example@v1.0.0", "osv", matches)) + + got, ok, err := cache.Get("pkg:golang/example@v1.0.0", "osv") + require.NoError(t, err) + assert.True(t, ok) + assert.Len(t, got, 1) + assert.Equal(t, "CVE-2024-1234", got[0].Vulnerability.ID) +} + +func TestCacheExpiry(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test.db") + cache, err := NewCache(dbPath, 1*time.Millisecond) + require.NoError(t, err) + defer cache.Close() + + matches := []types.VulnMatch{ + { + Package: types.Package{PURL: "pkg:golang/example@v1.0.0"}, + Vulnerability: types.Vulnerability{ID: "CVE-2024-1234", Source: "osv"}, + }, + } + + require.NoError(t, cache.Put("pkg:golang/example@v1.0.0", "osv", matches)) + time.Sleep(5 * time.Millisecond) + + _, ok, err := cache.Get("pkg:golang/example@v1.0.0", "osv") + require.NoError(t, err) + assert.False(t, ok) +} + +func TestCacheMiss(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test.db") + cache, err := NewCache(dbPath, 24*time.Hour) + require.NoError(t, err) + defer cache.Close() + + _, ok, err := cache.Get("pkg:golang/nonexistent@v1.0.0", "osv") + require.NoError(t, err) + assert.False(t, ok) +} + +func TestCacheOverwrite(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test.db") + cache, err := NewCache(dbPath, 24*time.Hour) + require.NoError(t, err) + defer cache.Close() + + old := []types.VulnMatch{ + {Vulnerability: types.Vulnerability{ID: "CVE-OLD"}}, + } + require.NoError(t, cache.Put("pkg:test@1.0.0", "osv", old)) + + updated := []types.VulnMatch{ + {Vulnerability: types.Vulnerability{ID: "CVE-NEW"}}, + } + require.NoError(t, cache.Put("pkg:test@1.0.0", "osv", updated)) + + got, ok, err := cache.Get("pkg:test@1.0.0", "osv") + require.NoError(t, err) + assert.True(t, ok) + assert.Equal(t, "CVE-NEW", got[0].Vulnerability.ID) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/client.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/client.go new file mode 100644 index 00000000..acf7d0eb --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/client.go @@ -0,0 +1,15 @@ +// ©AngelaMos | 2026 +// client.go + +package vuln + +import ( + "context" + + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +type Client interface { + Query(ctx context.Context, packages []types.Package) ([]types.VulnMatch, error) + Source() string +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cvss.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cvss.go new file mode 100644 index 00000000..ea229a6f --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cvss.go @@ -0,0 +1,177 @@ +// ©AngelaMos | 2026 +// cvss.go + +package vuln + +import ( + "math" + "strings" +) + +const ( + scopeUnchanged = 0.0 + scopeChanged = 1.0 +) + +type cvssMetrics struct { + AV float64 + AC float64 + PR float64 + UI float64 + S float64 + C float64 + I float64 + A float64 +} + +var avWeights = map[string]float64{ + "N": 0.85, + "A": 0.62, + "L": 0.55, + "P": 0.20, +} + +var acWeights = map[string]float64{ + "L": 0.77, + "H": 0.44, +} + +var prWeightsUnchanged = map[string]float64{ + "N": 0.85, + "L": 0.62, + "H": 0.27, +} + +var prWeightsChanged = map[string]float64{ + "N": 0.85, + "L": 0.68, + "H": 0.50, +} + +var uiWeights = map[string]float64{ + "N": 0.85, + "R": 0.62, +} + +var ciaWeights = map[string]float64{ + "H": 0.56, + "L": 0.22, + "N": 0.0, +} + +func parseCVSSVector(vector string) *cvssMetrics { + if !strings.HasPrefix(vector, "CVSS:3") { + return nil + } + + parts := strings.Split(vector, "/") + if len(parts) < 9 { + return nil + } + + vals := make(map[string]string, 8) + for _, part := range parts[1:] { + kv := strings.SplitN(part, ":", 2) + if len(kv) == 2 { + vals[kv[0]] = kv[1] + } + } + + required := []string{"AV", "AC", "PR", "UI", "S", "C", "I", "A"} + for _, key := range required { + if _, ok := vals[key]; !ok { + return nil + } + } + + av, ok := avWeights[vals["AV"]] + if !ok { + return nil + } + ac, ok := acWeights[vals["AC"]] + if !ok { + return nil + } + ui, ok := uiWeights[vals["UI"]] + if !ok { + return nil + } + c, ok := ciaWeights[vals["C"]] + if !ok { + return nil + } + i, ok := ciaWeights[vals["I"]] + if !ok { + return nil + } + a, ok := ciaWeights[vals["A"]] + if !ok { + return nil + } + + var s float64 + switch vals["S"] { + case "U": + s = scopeUnchanged + case "C": + s = scopeChanged + default: + return nil + } + + prMap := prWeightsUnchanged + if s == scopeChanged { + prMap = prWeightsChanged + } + pr, ok := prMap[vals["PR"]] + if !ok { + return nil + } + + return &cvssMetrics{ + AV: av, + AC: ac, + PR: pr, + UI: ui, + S: s, + C: c, + I: i, + A: a, + } +} + +func parseCVSSScore(vector string) float64 { + metrics := parseCVSSVector(vector) + if metrics == nil { + return 0 + } + + iss := 1 - ((1 - metrics.C) * (1 - metrics.I) * (1 - metrics.A)) + + var impact float64 + if metrics.S == scopeUnchanged { + impact = 6.42 * iss + } else { + impact = 7.52*(iss-0.029) - 3.25*math.Pow(iss-0.02, 15) + } + + if impact <= 0 { + return 0 + } + + exploitability := 8.22 * metrics.AV * metrics.AC * metrics.PR * metrics.UI + + var score float64 + if metrics.S == scopeUnchanged { + score = math.Min(impact+exploitability, 10) + } else { + score = math.Min(1.08*(impact+exploitability), 10) + } + + return cvssRoundUp(score) +} + +func cvssRoundUp(val float64) float64 { + shifted := math.Round(val*100000) / 100000 + return math.Ceil(shifted*10) / 10 +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cvss_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cvss_test.go new file mode 100644 index 00000000..110cdbe8 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/cvss_test.go @@ -0,0 +1,115 @@ +// ©AngelaMos | 2026 +// cvss_test.go + +package vuln + +import ( + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" +) + +func TestCVSSKnownVectors(t *testing.T) { + tests := []struct { + name string + vector string + score float64 + }{ + { + name: "CVE-2023-44487 HTTP/2 rapid reset", + vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + score: 7.5, + }, + { + name: "max severity all high", + vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + score: 10.0, + }, + { + name: "log4shell CVE-2021-44228", + vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + score: 10.0, + }, + { + name: "local low impact", + vector: "CVSS:3.1/AV:L/AC:H/PR:H/UI:R/S:U/C:L/I:N/A:N", + score: 1.8, + }, + { + name: "network medium complexity", + vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + score: 8.1, + }, + { + name: "scope changed low priv", + vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N", + score: 6.4, + }, + { + name: "physical access required", + vector: "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + score: 6.8, + }, + { + name: "adjacent network", + vector: "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + score: 8.8, + }, + { + name: "scope changed high priv", + vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H", + score: 8.8, + }, + { + name: "all none impact", + vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N", + score: 0.0, + }, + { + name: "CVE-2022-41723 net/http2 resource", + vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + score: 7.5, + }, + { + name: "CVSS 3.0 prefix also supported", + vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + score: 9.8, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseCVSSScore(tt.vector) + assert.InDelta(t, tt.score, got, 0.1, + "vector=%s expected=%.1f got=%.1f", tt.vector, tt.score, got) + }) + } +} + +func TestCVSSInvalidVectors(t *testing.T) { + assert.Equal(t, 0.0, parseCVSSScore("")) + assert.Equal(t, 0.0, parseCVSSScore("not-a-vector")) + assert.Equal(t, 0.0, parseCVSSScore("CVSS:2.0/AV:N")) + assert.Equal(t, 0.0, parseCVSSScore("CVSS:3.1/AV:N")) + assert.Equal(t, 0.0, parseCVSSScore("CVSS:3.1/AV:X/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")) +} + +func TestCVSSRoundUp(t *testing.T) { + assert.Equal(t, 4.1, cvssRoundUp(4.02)) + assert.Equal(t, 4.0, cvssRoundUp(4.0)) + assert.Equal(t, 0.0, cvssRoundUp(0.0)) + assert.Equal(t, 10.0, cvssRoundUp(10.0)) +} + +func TestScoreToSeverity(t *testing.T) { + assert.Equal(t, types.SeverityCritical, scoreToSeverity(9.8)) + assert.Equal(t, types.SeverityCritical, scoreToSeverity(9.0)) + assert.Equal(t, types.SeverityHigh, scoreToSeverity(8.9)) + assert.Equal(t, types.SeverityHigh, scoreToSeverity(7.0)) + assert.Equal(t, types.SeverityMedium, scoreToSeverity(6.9)) + assert.Equal(t, types.SeverityMedium, scoreToSeverity(4.0)) + assert.Equal(t, types.SeverityLow, scoreToSeverity(3.9)) + assert.Equal(t, types.SeverityLow, scoreToSeverity(0.1)) + assert.Equal(t, types.SeverityNone, scoreToSeverity(0.0)) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/nvd.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/nvd.go new file mode 100644 index 00000000..519da0ee --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/nvd.go @@ -0,0 +1,220 @@ +// ©AngelaMos | 2026 +// nvd.go + +package vuln + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/CarterPerez-dev/bomber/internal/config" + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +type NVDClient struct { + baseURL string + apiKey string + httpClient *http.Client + mu sync.Mutex + lastReq time.Time + rateDelay time.Duration +} + +type nvdOption func(*NVDClient) + +func WithNVDBaseURL(url string) nvdOption { + return func(c *NVDClient) { + c.baseURL = url + } +} + +func WithNVDAPIKey(key string) nvdOption { + return func(c *NVDClient) { + c.apiKey = key + c.rateDelay = config.NVDRateWithKey + } +} + +func NewNVDClient(opts ...nvdOption) *NVDClient { + c := &NVDClient{ + baseURL: config.NVDBaseURL, + rateDelay: config.NVDRateWithoutKey, + httpClient: &http.Client{ + Timeout: config.HTTPTimeout, + }, + } + for _, opt := range opts { + opt(c) + } + return c +} + +func (c *NVDClient) Source() string { + return config.NVDSourceName +} + +func (c *NVDClient) Query(ctx context.Context, packages []types.Package) ([]types.VulnMatch, error) { + if len(packages) == 0 { + return nil, nil + } + + var allMatches []types.VulnMatch + + for _, pkg := range packages { + if err := ctx.Err(); err != nil { + return allMatches, err + } + + c.rateLimit(ctx) + + matches, err := c.queryPackage(ctx, pkg) + if err != nil { + continue + } + allMatches = append(allMatches, matches...) + } + + return allMatches, nil +} + +func (c *NVDClient) queryPackage(ctx context.Context, pkg types.Package) ([]types.VulnMatch, error) { + params := url.Values{} + params.Set("virtualMatchString", buildCPEString(pkg)) + + reqURL := c.baseURL + "?" + params.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return nil, fmt.Errorf("create nvd request: %w", err) + } + + if c.apiKey != "" { + req.Header.Set("apiKey", c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("nvd http request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("nvd api error %d: %s", resp.StatusCode, string(body)) + } + + var nvdResp nvdResponse + if err := json.NewDecoder(resp.Body).Decode(&nvdResp); err != nil { + return nil, fmt.Errorf("decode nvd response: %w", err) + } + + var matches []types.VulnMatch + for _, item := range nvdResp.Vulnerabilities { + cve := item.CVE + published, _ := time.Parse(time.RFC3339, cve.Published) + match := types.VulnMatch{ + Package: pkg, + Vulnerability: types.Vulnerability{ + ID: cve.ID, + Summary: extractDescription(cve.Descriptions), + Source: config.NVDSourceName, + Published: published, + }, + } + + if len(cve.Metrics.CVSSV31) > 0 { + metric := cve.Metrics.CVSSV31[0] + match.Vulnerability.Score = metric.Data.BaseScore + match.Vulnerability.Severity = types.ParseSeverity(metric.Data.BaseSeverity) + } + + matches = append(matches, match) + } + + return matches, nil +} + +func buildCPEString(pkg types.Package) string { + product := pkg.Name + if idx := strings.LastIndex(product, "/"); idx >= 0 { + product = product[idx+1:] + } + product = strings.ToLower(product) + + version := strings.TrimPrefix(pkg.Version, "v") + if version == "" { + version = "*" + } + + return fmt.Sprintf("cpe:2.3:a:*:%s:%s:*:*:*:*:*:*:*", product, version) +} + +func (c *NVDClient) rateLimit(ctx context.Context) { + c.mu.Lock() + defer c.mu.Unlock() + + elapsed := time.Since(c.lastReq) + if elapsed < c.rateDelay { + wait := c.rateDelay - elapsed + timer := time.NewTimer(wait) + defer timer.Stop() + + select { + case <-ctx.Done(): + return + case <-timer.C: + } + } + c.lastReq = time.Now() +} + +type nvdResponse struct { + Vulnerabilities []nvdVulnItem `json:"vulnerabilities"` +} + +type nvdVulnItem struct { + CVE nvdCVE `json:"cve"` +} + +type nvdCVE struct { + ID string `json:"id"` + Published string `json:"published"` + Descriptions []nvdDescription `json:"descriptions"` + Metrics nvdMetrics `json:"metrics"` +} + +type nvdDescription struct { + Lang string `json:"lang"` + Value string `json:"value"` +} + +type nvdMetrics struct { + CVSSV31 []nvdCVSSV31 `json:"cvssMetricV31"` +} + +type nvdCVSSV31 struct { + Data nvdCVSSData `json:"cvssData"` +} + +type nvdCVSSData struct { + BaseScore float64 `json:"baseScore"` + BaseSeverity string `json:"baseSeverity"` +} + +func extractDescription(descs []nvdDescription) string { + for _, d := range descs { + if d.Lang == "en" { + return d.Value + } + } + if len(descs) > 0 { + return descs[0].Value + } + return "" +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/nvd_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/nvd_test.go new file mode 100644 index 00000000..1adcc7b1 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/nvd_test.go @@ -0,0 +1,120 @@ +// ©AngelaMos | 2026 +// nvd_test.go + +package vuln + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNVDQuery(t *testing.T) { + nvdFixture := `{ + "vulnerabilities": [ + { + "cve": { + "id": "CVE-2023-44487", + "published": "2023-10-10T14:15:00.000", + "descriptions": [ + {"lang": "en", "value": "HTTP/2 rapid reset attack"} + ], + "metrics": { + "cvssMetricV31": [ + { + "cvssData": { + "baseScore": 7.5, + "baseSeverity": "HIGH" + } + } + ] + } + } + } + ] + }` + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.URL.RawQuery, "virtualMatchString") + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(nvdFixture)) + })) + defer server.Close() + + client := NewNVDClient(WithNVDBaseURL(server.URL)) + packages := []types.Package{ + {Name: "golang.org/x/net", Version: "v0.1.0", Ecosystem: types.EcosystemGo, PURL: "pkg:golang/golang.org/x/net@v0.1.0"}, + } + + matches, err := client.Query(context.Background(), packages) + require.NoError(t, err) + require.NotEmpty(t, matches) + assert.Equal(t, "CVE-2023-44487", matches[0].Vulnerability.ID) + assert.Equal(t, "nvd", matches[0].Vulnerability.Source) + assert.Equal(t, 7.5, matches[0].Vulnerability.Score) + assert.Equal(t, types.SeverityHigh, matches[0].Vulnerability.Severity) +} + +func TestNVDAPIKeyHeader(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "test-key-123", r.Header.Get("apiKey")) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"vulnerabilities": []}`)) + })) + defer server.Close() + + client := NewNVDClient(WithNVDBaseURL(server.URL), WithNVDAPIKey("test-key-123")) + packages := []types.Package{ + {Name: "test-pkg", Ecosystem: types.EcosystemNode, PURL: "pkg:npm/test-pkg@1.0.0"}, + } + _, err := client.Query(context.Background(), packages) + require.NoError(t, err) +} + +func TestNVDEmptyPackages(t *testing.T) { + client := NewNVDClient() + matches, err := client.Query(context.Background(), nil) + require.NoError(t, err) + assert.Empty(t, matches) +} + +func TestNVDSource(t *testing.T) { + client := NewNVDClient() + assert.Equal(t, "nvd", client.Source()) +} + +func TestNVDBuildCPEString(t *testing.T) { + tests := []struct { + name string + pkg types.Package + contains string + }{ + { + name: "go module", + pkg: types.Package{Name: "golang.org/x/net", Version: "v0.1.0", Ecosystem: types.EcosystemGo}, + contains: "cpe:2.3:a:*:net:0.1.0", + }, + { + name: "npm package", + pkg: types.Package{Name: "express", Version: "4.18.2", Ecosystem: types.EcosystemNode}, + contains: "cpe:2.3:a:*:express:4.18.2", + }, + { + name: "pypi package", + pkg: types.Package{Name: "requests", Version: "2.31.0", Ecosystem: types.EcosystemPython}, + contains: "cpe:2.3:a:*:requests:2.31.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cpe := buildCPEString(tt.pkg) + assert.Contains(t, cpe, tt.contains) + }) + } +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/osv.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/osv.go new file mode 100644 index 00000000..3c4aef2a --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/osv.go @@ -0,0 +1,264 @@ +// ©AngelaMos | 2026 +// osv.go + +package vuln + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/CarterPerez-dev/bomber/internal/config" + "github.com/CarterPerez-dev/bomber/pkg/types" +) + +type OSVClient struct { + baseURL string + httpClient *http.Client +} + +type osvOption func(*OSVClient) + +func WithOSVBaseURL(url string) osvOption { + return func(c *OSVClient) { + c.baseURL = url + } +} + +func NewOSVClient(opts ...osvOption) *OSVClient { + c := &OSVClient{ + baseURL: config.OSVBaseURL, + httpClient: &http.Client{ + Timeout: config.HTTPTimeout, + }, + } + for _, opt := range opts { + opt(c) + } + return c +} + +func (c *OSVClient) Source() string { + return config.OSVSourceName +} + +func (c *OSVClient) Query(ctx context.Context, packages []types.Package) ([]types.VulnMatch, error) { + if len(packages) == 0 { + return nil, nil + } + + var allMatches []types.VulnMatch + + for i := 0; i < len(packages); i += config.OSVBatchSize { + if err := ctx.Err(); err != nil { + return allMatches, err + } + + end := i + config.OSVBatchSize + if end > len(packages) { + end = len(packages) + } + batch := packages[i:end] + + matches, err := c.queryBatch(ctx, batch) + if err != nil { + return allMatches, fmt.Errorf("osv batch query: %w", err) + } + allMatches = append(allMatches, matches...) + } + + return allMatches, nil +} + +func (c *OSVClient) queryBatch(ctx context.Context, packages []types.Package) ([]types.VulnMatch, error) { + queries := make([]osvQuery, len(packages)) + for i, pkg := range packages { + queries[i] = osvQuery{ + Package: osvQueryPackage{ + PURL: pkg.PURL, + }, + } + } + + reqBody := osvBatchRequest{Queries: queries} + body, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("marshal request: %w", err) + } + + url := c.baseURL + "/v1/querybatch" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("http request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("osv api error %d: %s", resp.StatusCode, string(respBody)) + } + + var batchResp osvBatchResponse + if err := json.NewDecoder(resp.Body).Decode(&batchResp); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + + var matches []types.VulnMatch + for i, result := range batchResp.Results { + if i >= len(packages) { + break + } + pkg := packages[i] + for _, v := range result.Vulns { + published, _ := time.Parse(time.RFC3339, v.Published) + match := types.VulnMatch{ + Package: pkg, + Vulnerability: types.Vulnerability{ + ID: v.ID, + Aliases: v.Aliases, + Summary: v.Summary, + Source: config.OSVSourceName, + Severity: parseSeverityFromOSV(v), + Score: parseScoreFromOSV(v), + Published: published, + }, + } + + if len(v.Affected) > 0 && len(v.Affected[0].Ranges) > 0 { + r := v.Affected[0].Ranges[0] + match.Vulnerability.AffectedRange = formatRange(r.Events) + match.Vulnerability.FixVersion = extractFixVersion(r.Events) + } + + matches = append(matches, match) + } + } + + return matches, nil +} + +type osvBatchRequest struct { + Queries []osvQuery `json:"queries"` +} + +type osvQuery struct { + Package osvQueryPackage `json:"package"` +} + +type osvQueryPackage struct { + PURL string `json:"purl"` +} + +type osvBatchResponse struct { + Results []osvResult `json:"results"` +} + +type osvResult struct { + Vulns []osvVuln `json:"vulns"` +} + +type osvVuln struct { + ID string `json:"id"` + Summary string `json:"summary"` + Published string `json:"published"` + Aliases []string `json:"aliases"` + Severity []osvSeverity `json:"severity"` + Affected []osvAffected `json:"affected"` + DBSpec struct { + Severity string `json:"severity"` + } `json:"database_specific"` +} + +type osvSeverity struct { + Type string `json:"type"` + Score string `json:"score"` +} + +type osvAffected struct { + Package struct { + PURL string `json:"purl"` + } `json:"package"` + Ranges []osvRange `json:"ranges"` +} + +type osvRange struct { + Type string `json:"type"` + Events []osvEvent `json:"events"` +} + +type osvEvent struct { + Introduced string `json:"introduced,omitempty"` + Fixed string `json:"fixed,omitempty"` +} + +func parseSeverityFromOSV(v osvVuln) types.Severity { + if v.DBSpec.Severity != "" { + return types.ParseSeverity(v.DBSpec.Severity) + } + + for _, s := range v.Severity { + if s.Type == "CVSS_V3" { + score := parseCVSSScore(s.Score) + return scoreToSeverity(score) + } + } + + return types.SeverityNone +} + +func parseScoreFromOSV(v osvVuln) float64 { + for _, s := range v.Severity { + if s.Type == "CVSS_V3" { + return parseCVSSScore(s.Score) + } + } + return 0 +} + +func scoreToSeverity(score float64) types.Severity { + switch { + case score >= 9.0: + return types.SeverityCritical + case score >= 7.0: + return types.SeverityHigh + case score >= 4.0: + return types.SeverityMedium + case score > 0: + return types.SeverityLow + default: + return types.SeverityNone + } +} + +func formatRange(events []osvEvent) string { + var parts []string + for _, e := range events { + if e.Introduced != "" { + parts = append(parts, ">= "+e.Introduced) + } + if e.Fixed != "" { + parts = append(parts, "< "+e.Fixed) + } + } + return strings.Join(parts, ", ") +} + +func extractFixVersion(events []osvEvent) string { + for _, e := range events { + if e.Fixed != "" { + return e.Fixed + } + } + return "" +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/osv_test.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/osv_test.go new file mode 100644 index 00000000..b0f4c289 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/internal/vuln/osv_test.go @@ -0,0 +1,123 @@ +// ©AngelaMos | 2026 +// osv_test.go + +package vuln + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/CarterPerez-dev/bomber/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func fixtureDir(t *testing.T) string { + t.Helper() + _, filename, _, ok := runtime.Caller(0) + require.True(t, ok) + return filepath.Join(filepath.Dir(filename), "..", "..", "testdata", "vuln-responses") +} + +func TestOSVQueryBatch(t *testing.T) { + fixture, err := os.ReadFile(filepath.Join(fixtureDir(t), "osv-batch.json")) + require.NoError(t, err) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/v1/querybatch", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(fixture) + })) + defer server.Close() + + client := NewOSVClient(WithOSVBaseURL(server.URL)) + + packages := []types.Package{ + {Name: "golang.org/x/net", Version: "v0.1.0", PURL: "pkg:golang/golang.org/x/net@v0.1.0"}, + } + + matches, err := client.Query(context.Background(), packages) + require.NoError(t, err) + require.NotEmpty(t, matches) + assert.Equal(t, "osv", matches[0].Vulnerability.Source) + assert.Equal(t, "GO-2023-2102", matches[0].Vulnerability.ID) +} + +func TestOSVMatchHasFixVersion(t *testing.T) { + fixture, err := os.ReadFile(filepath.Join(fixtureDir(t), "osv-batch.json")) + require.NoError(t, err) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(fixture) + })) + defer server.Close() + + client := NewOSVClient(WithOSVBaseURL(server.URL)) + packages := []types.Package{ + {Name: "golang.org/x/net", Version: "v0.1.0", PURL: "pkg:golang/golang.org/x/net@v0.1.0"}, + } + + matches, err := client.Query(context.Background(), packages) + require.NoError(t, err) + require.NotEmpty(t, matches) + assert.Equal(t, "0.17.0", matches[0].Vulnerability.FixVersion) +} + +func TestOSVMatchHasCVSSScore(t *testing.T) { + fixture, err := os.ReadFile(filepath.Join(fixtureDir(t), "osv-batch.json")) + require.NoError(t, err) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(fixture) + })) + defer server.Close() + + client := NewOSVClient(WithOSVBaseURL(server.URL)) + packages := []types.Package{ + {Name: "golang.org/x/net", Version: "v0.1.0", PURL: "pkg:golang/golang.org/x/net@v0.1.0"}, + } + + matches, err := client.Query(context.Background(), packages) + require.NoError(t, err) + require.NotEmpty(t, matches) + assert.Equal(t, 7.5, matches[0].Vulnerability.Score) +} + +func TestOSVEmptyPackages(t *testing.T) { + client := NewOSVClient() + matches, err := client.Query(context.Background(), nil) + require.NoError(t, err) + assert.Empty(t, matches) +} + +func TestOSVSource(t *testing.T) { + client := NewOSVClient() + assert.Equal(t, "osv", client.Source()) +} + +func TestOSVCancelledContext(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"results": []}`)) + })) + defer server.Close() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + client := NewOSVClient(WithOSVBaseURL(server.URL)) + packages := []types.Package{ + {Name: "test", PURL: "pkg:test/test@1.0.0"}, + } + + _, err := client.Query(ctx, packages) + assert.Error(t, err) +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/pkg/types/types.go b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/pkg/types/types.go new file mode 100644 index 00000000..ea81e3f2 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/pkg/types/types.go @@ -0,0 +1,160 @@ +// ©AngelaMos | 2026 +// types.go + +package types + +import ( + "strings" + "time" +) + +type Ecosystem int + +const ( + EcosystemGo Ecosystem = iota + EcosystemNode + EcosystemPython +) + +func (e Ecosystem) String() string { + switch e { + case EcosystemGo: + return "go" + case EcosystemNode: + return "node" + case EcosystemPython: + return "python" + default: + return "unknown" + } +} + +type Severity int + +const ( + SeverityNone Severity = iota + SeverityLow + SeverityMedium + SeverityHigh + SeverityCritical +) + +func (s Severity) String() string { + switch s { + case SeverityCritical: + return "CRITICAL" + case SeverityHigh: + return "HIGH" + case SeverityMedium: + return "MEDIUM" + case SeverityLow: + return "LOW" + case SeverityNone: + return "NONE" + default: + return "UNKNOWN" + } +} + +func ParseSeverity(s string) Severity { + switch strings.ToUpper(s) { + case "CRITICAL": + return SeverityCritical + case "HIGH": + return SeverityHigh + case "MEDIUM": + return SeverityMedium + case "LOW": + return SeverityLow + case "NONE": + return SeverityNone + default: + return SeverityNone + } +} + +func (s Severity) Rank() int { + return int(s) +} + +type Checksum struct { + Algorithm string + Value string +} + +type Package struct { + Name string + Version string + Ecosystem Ecosystem + PURL string + Checksums []Checksum + Direct bool + DepthLevel int +} + +type DependencyGraph struct { + Root Package + Nodes map[string]Package + Edges map[string][]string +} + +func NewDependencyGraph(root Package) *DependencyGraph { + g := &DependencyGraph{ + Root: root, + Nodes: make(map[string]Package), + Edges: make(map[string][]string), + } + g.Nodes[root.PURL] = root + return g +} + +func (g *DependencyGraph) AddPackage(pkg Package) { + g.Nodes[pkg.PURL] = pkg +} + +func (g *DependencyGraph) AddEdge(parentPURL, childPURL string) { + g.Edges[parentPURL] = append(g.Edges[parentPURL], childPURL) +} + +type Vulnerability struct { + ID string + Aliases []string + Severity Severity + Score float64 + AffectedRange string + FixVersion string + Summary string + Source string + Published time.Time +} + +type VulnMatch struct { + Package Package + Vulnerability Vulnerability +} + +type PolicyViolation struct { + Rule string + Message string + Package Package + Vuln *Vulnerability +} + +type ScanResult struct { + Graphs []*DependencyGraph + TotalPkgs int + DirectPkgs int + Ecosystems []Ecosystem +} + +type VulnReport struct { + Matches []VulnMatch + TotalPkgs int + DirectPkgs int + BySeverity map[Severity]int +} + +type CheckResult struct { + Passed bool + Violations []PolicyViolation +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/empty-project/.gitkeep b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/empty-project/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/go-project/go.mod b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/go-project/go.mod new file mode 100644 index 00000000..380051cb --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/go-project/go.mod @@ -0,0 +1,13 @@ +module example.com/testproject + +go 1.24.4 + +require ( + github.com/spf13/cobra v1.10.2 + golang.org/x/net v0.1.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect +) diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/go-project/go.sum b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/go-project/go.sum new file mode 100644 index 00000000..b44c26e6 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/go-project/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUQYwXIPsb6RGxoLOEECLDgLA6tE0= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jGbOGsM= +github.com/spf13/cobra v1.10.2 h1:2CIUWEyHTv2WNHK+WwTGTEk0FfMJJGf1sP9JL8G8bQ= +github.com/spf13/cobra v1.10.2/go.mod h1:PfRYMMFHRAxnQGpE+WVPF+cSfnkmCnXanEqJbMvEB+4= +github.com/spf13/pflag v1.0.10 h1:iy+VFUOCP1a+8yFto/drg2CJ5YMXRVfIwvu3nHamp6g= +github.com/spf13/pflag v1.0.10/go.mod h1:n1GLbMFMk0PZ9JbCktcMRbaTMMWf9MMS1XjlCPiAb4Y= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7e0vtdQ034QjbX2ELhuB+T7Rn/HBdew= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/frontend/package.json b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/frontend/package.json new file mode 100644 index 00000000..5cc541e4 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/frontend/package.json @@ -0,0 +1,7 @@ +{ + "name": "monorepo-frontend", + "version": "1.0.0", + "dependencies": { + "react": "19.2.1" + } +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/frontend/pnpm-lock.yaml b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..ca221d98 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/frontend/pnpm-lock.yaml @@ -0,0 +1,20 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + react: + specifier: 19.2.1 + version: 19.2.1 + +packages: + react@19.2.1: + resolution: {integrity: sha512-T0bR1fHknGmLUFZLDmAXMmkiRx5s6V+1HJfOwkaXPgo3WieNQ3vY4BmPOcAlKm0O0GsrGP0icGo0N0a/4QcKg==} + engines: {node: '>=0.10.0'} + +snapshots: + react@19.2.1: {} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/go.mod b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/go.mod new file mode 100644 index 00000000..ec3ae217 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/go.mod @@ -0,0 +1,5 @@ +module example.com/monorepo + +go 1.24.4 + +require golang.org/x/sync v0.19.0 diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/go.sum b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/go.sum new file mode 100644 index 00000000..82c3b010 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/monorepo/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.19.0 h1:ENBSLSj5SicIJoAUhWOkkBTCBbqMm72MzO4BnMnTHMo= +golang.org/x/sync v0.19.0/go.mod h1:JkUqWJITmSJNOFnwMNHY1bJMEMx74se/cGD0+dii0to= diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/node-project/package.json b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/node-project/package.json new file mode 100644 index 00000000..0156e354 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/node-project/package.json @@ -0,0 +1,11 @@ +{ + "name": "test-project", + "version": "1.0.0", + "dependencies": { + "express": "4.18.2", + "lodash": "4.17.20" + }, + "devDependencies": { + "typescript": "5.3.3" + } +} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/node-project/pnpm-lock.yaml b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/node-project/pnpm-lock.yaml new file mode 100644 index 00000000..1d584fc1 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/node-project/pnpm-lock.yaml @@ -0,0 +1,69 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + express: + specifier: 4.18.2 + version: 4.18.2 + lodash: + specifier: 4.17.20 + version: 4.17.20 + devDependencies: + typescript: + specifier: 5.3.3 + version: 5.3.3 + +packages: + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + cookie@0.5.0: + resolution: {integrity: sha512-YGG3ejvBNHRqu0559dc2l7RJhAt10gLfA5/UQqa5LoT6CrWnB3VEFIiDmYKKJvPqbDSIIqNhjR8GWtkJ8QNYRg==} + engines: {node: '>= 0.6'} + + express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + + lodash@4.17.20: + resolution: {integrity: sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRG5XhaY1B6l4zc7HjDtY9BV6YBN48URNR0500Q==} + + semver@7.3.7: + resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} + engines: {node: '>=10'} + hasBin: true + + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + accepts@1.3.8: {} + + body-parser@1.20.1: {} + + cookie@0.5.0: {} + + express@4.18.2: + dependencies: + accepts: 1.3.8 + body-parser: 1.20.1 + cookie: 0.5.0 + semver: 7.3.7 + + lodash@4.17.20: {} + + semver@7.3.7: {} + + typescript@5.3.3: {} diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/python-project/pyproject.toml b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/python-project/pyproject.toml new file mode 100644 index 00000000..a9b45c11 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/python-project/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "test-project" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "requests>=2.31.0", + "pydantic>=2.5.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", +] diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/python-project/uv.lock b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/python-project/uv.lock new file mode 100644 index 00000000..c6dd1ce3 --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/python-project/uv.lock @@ -0,0 +1,100 @@ +version = 1 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "certifi" +version = "2024.2.2" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "idna" +version = "3.6" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "packaging" +version = "24.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "pluggy" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "pydantic" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "pydantic-core" +version = "2.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] + +[[package]] +name = "pytest" +version = "8.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] + +[[package]] +name = "requests" +version = "2.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] + +[[package]] +name = "test-project" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pydantic" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "urllib3" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } diff --git a/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/vuln-responses/osv-batch.json b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/vuln-responses/osv-batch.json new file mode 100644 index 00000000..a5b1924b --- /dev/null +++ b/PROJECTS/intermediate/sbom-generator-vulnerability-matcher/testdata/vuln-responses/osv-batch.json @@ -0,0 +1,72 @@ +{ + "results": [ + { + "vulns": [ + { + "id": "GO-2023-2102", + "summary": "HTTP/2 rapid reset can cause excessive work in net/http", + "aliases": ["CVE-2023-44487", "GHSA-qppj-fm5r-hxr3"], + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "affected": [ + { + "package": { + "ecosystem": "Go", + "name": "golang.org/x/net", + "purl": "pkg:golang/golang.org/x/net" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + {"introduced": "0"}, + {"fixed": "0.17.0"} + ] + } + ] + } + ], + "database_specific": { + "severity": "HIGH" + } + }, + { + "id": "GO-2023-1571", + "summary": "Excessive resource consumption in net/http", + "aliases": ["CVE-2022-41723"], + "severity": [ + { + "type": "CVSS_V3", + "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + ], + "affected": [ + { + "package": { + "ecosystem": "Go", + "name": "golang.org/x/net", + "purl": "pkg:golang/golang.org/x/net" + }, + "ranges": [ + { + "type": "SEMVER", + "events": [ + {"introduced": "0"}, + {"fixed": "0.7.0"} + ] + } + ] + } + ], + "database_specific": { + "severity": "HIGH" + } + } + ] + } + ] +} diff --git a/TEMPLATES/fullstack-template b/TEMPLATES/fullstack-template index daa74d16..ecbb534e 160000 --- a/TEMPLATES/fullstack-template +++ b/TEMPLATES/fullstack-template @@ -1 +1 @@ -Subproject commit daa74d165fa60faaa41aac493f2ba1a3400a88c4 +Subproject commit ecbb534e85e8e381e6e89aece4b786db8f7ad172