From 8cb9dbd906fb131716c524a52bf2196e045fba32 Mon Sep 17 00:00:00 2001 From: Vijay Yadav <135238089+vijayleo31@users.noreply.github.com> Date: Sat, 30 Aug 2025 23:43:47 +0530 Subject: [PATCH] Cybersecuritypythonrubyproject --- Cybersecuritypythonrubyproject | 562 +++++++++++++++++++++++++++++++++ 1 file changed, 562 insertions(+) create mode 100644 Cybersecuritypythonrubyproject diff --git a/Cybersecuritypythonrubyproject b/Cybersecuritypythonrubyproject new file mode 100644 index 00000000..1f42f7ab --- /dev/null +++ b/Cybersecuritypythonrubyproject @@ -0,0 +1,562 @@ +# Project: Docker Cyber Analyzer (AI-ish rules + baseline checks) + +A minimal, extensible project that inspects your local Docker environment for risk signals often present in cybercrime/abuse investigations (e.g., privileged containers, root users, broad port exposure, suspicious secrets in env). Results are stored in SQLite and are accessible via a Python FastAPI and a Ruby Sinatra dashboard. + +> **Note**: You can enrich the Python analyzer with ML/AI later (e.g., scoring findings, anomaly detection, or rule-learning from labeled cases). The current build uses a deterministic ruleset that is easy to extend. + +--- + +## File tree + +``` +/docker-cyber-analyzer +├─ README.md +├─ docker-compose.yml +├─ data/ # SQLite file lives here (shared volume) +├─ python_service/ +│ ├─ requirements.txt +│ ├─ Dockerfile +│ ├─ database.py +│ ├─ models.py +│ ├─ analyzer.py # Core checks +│ └─ app.py # FastAPI server +└─ ruby_dashboard/ + ├─ Gemfile + ├─ Dockerfile + └─ app.rb # Sinatra mini UI +``` + +--- + +## Quick start (Docker Compose) + +```bash +# In project root +docker compose up --build + +# Trigger a scan +curl -X POST http://localhost:8000/analyze + +# Explore results +# API docs: http://localhost:8000/docs +# Dashboard: http://localhost:4567 +``` + +--- + +## Python service (FastAPI) + +### `python_service/requirements.txt` + +```txt +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +pydantic==2.9.2 +SQLAlchemy==2.0.35 +docker==7.1.0 +``` + +### `python_service/Dockerfile` + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENV PYTHONUNBUFFERED=1 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +### `python_service/database.py` + +```python +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase + +DB_PATH = os.getenv("DB_PATH", "/app/data/analyzer.db") + +class Base(DeclarativeBase): + pass + +engine = create_engine(f"sqlite:///{DB_PATH}", echo=False, future=True) +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) +``` + +### `python_service/models.py` + +```python +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Float +from sqlalchemy.orm import relationship, Mapped, mapped_column +from datetime import datetime +from database import Base + +class Case(Base): + __tablename__ = "cases" + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + note: Mapped[str] = mapped_column(String(255), default="auto-scan") + + containers = relationship("ContainerReport", back_populates="case", cascade="all, delete-orphan") + images = relationship("ImageReport", back_populates="case", cascade="all, delete-orphan") + findings = relationship("Finding", back_populates="case", cascade="all, delete-orphan") + +class ContainerReport(Base): + __tablename__ = "container_reports" + id: Mapped[int] = mapped_column(Integer, primary_key=True) + case_id: Mapped[int] = mapped_column(Integer, ForeignKey("cases.id")) + container_id: Mapped[str] = mapped_column(String(128)) + name: Mapped[str] = mapped_column(String(256)) + image: Mapped[str] = mapped_column(String(256)) + privileged: Mapped[bool] = mapped_column(Boolean, default=False) + runs_as_root: Mapped[bool] = mapped_column(Boolean, default=False) + host_network: Mapped[bool] = mapped_column(Boolean, default=False) + port_0_0_0_0: Mapped[bool] = mapped_column(Boolean, default=False) + added_caps: Mapped[str] = mapped_column(Text, default="") + mounts: Mapped[str] = mapped_column(Text, default="") + suspicious_env: Mapped[str] = mapped_column(Text, default="") + + case = relationship("Case", back_populates="containers") + +class ImageReport(Base): + __tablename__ = "image_reports" + id: Mapped[int] = mapped_column(Integer, primary_key=True) + case_id: Mapped[int] = mapped_column(Integer, ForeignKey("cases.id")) + image_id: Mapped[str] = mapped_column(String(128)) + repo_tags: Mapped[str] = mapped_column(Text, default="") + size_mb: Mapped[float] = mapped_column(Float, default=0.0) + latest_tag: Mapped[bool] = mapped_column(Boolean, default=False) + age_days: Mapped[float] = mapped_column(Float, default=0.0) + + case = relationship("Case", back_populates="images") + +class Finding(Base): + __tablename__ = "findings" + id: Mapped[int] = mapped_column(Integer, primary_key=True) + case_id: Mapped[int] = mapped_column(Integer, ForeignKey("cases.id")) + subject_type: Mapped[str] = mapped_column(String(32)) # 'container' | 'image' | 'system' + subject_id: Mapped[str] = mapped_column(String(128)) + severity: Mapped[str] = mapped_column(String(16)) # LOW/MEDIUM/HIGH/CRITICAL + title: Mapped[str] = mapped_column(String(256)) + detail: Mapped[str] = mapped_column(Text, default="") + + case = relationship("Case", back_populates="findings") +``` + +### `python_service/analyzer.py` + +```python +import os, re, datetime +from typing import List, Dict, Any +import docker +from sqlalchemy.orm import Session +from database import SessionLocal, engine +from models import Base, Case, ContainerReport, ImageReport, Finding + +SUS_ENV_PATTERNS = [ + r"AWS_(ACCESS_KEY|SECRET|SESSION)_?ID?", r"API_KEY", r"SECRET", r"TOKEN", + r"PASSWORD", r"PRIVATE_KEY", r"GCP_|GOOGLE_APPLICATION_CREDENTIALS", r"AZURE_" +] + +IMAGE_AGE_LIMIT_DAYS = int(os.getenv("ANALYZER_IMAGE_AGE_DAYS", "365")) +IMAGE_SIZE_LIMIT_MB = int(os.getenv("ANALYZER_IMAGE_SIZE_MB", "800")) + +def init_db(): + Base.metadata.create_all(bind=engine) + +def matches_suspicious(key: str) -> bool: + for p in SUS_ENV_PATTERNS: + if re.search(p, key, flags=re.IGNORECASE): + return True + return False + +def analyze() -> int: + init_db() + client = docker.from_env() + with SessionLocal() as db: + case = Case() + db.add(case) + db.commit() + db.refresh(case) + + analyze_containers(client, db, case.id) + analyze_images(client, db, case.id) + db.commit() + return case.id + +def analyze_containers(client, db: Session, case_id: int): + for c in client.containers.list(all=True): + attrs = c.attrs + cfg = attrs.get("Config", {}) or {} + host_cfg = attrs.get("HostConfig", {}) or {} + network_mode = host_cfg.get("NetworkMode") + privileged = bool(host_cfg.get("Privileged")) + added_caps = host_cfg.get("CapAdd") or [] + user = cfg.get("User") or "" + runs_as_root = (user.strip() == "" or user.strip() == "0") + ports = attrs.get("NetworkSettings", {}).get("Ports") or {} + port_0_0_0_0 = any(bind for p in ports.values() if p for bind in p if bind.get("HostIp") in ("0.0.0.0", "::")) + host_network = (network_mode == "host") + mounts = attrs.get("Mounts") or [] + mounts_str = "; ".join([f"{m.get('Source')} -> {m.get('Destination')}" for m in mounts]) + + env_list = cfg.get("Env") or [] + env_keys = [e.split("=", 1)[0] for e in env_list] + sus_keys = [k for k in env_keys if matches_suspicious(k)] + suspicious_env = ", ".join(sus_keys) + + report = ContainerReport( + case_id=case_id, + container_id=c.id[:12], + name=(attrs.get("Name") or c.name or "").lstrip("/"), + image=cfg.get("Image") or c.image.tags[0] if c.image.tags else c.image.short_id, + privileged=privileged, + runs_as_root=runs_as_root, + host_network=host_network, + port_0_0_0_0=port_0_0_0_0, + added_caps=",".join(added_caps), + mounts=mounts_str, + suspicious_env=suspicious_env, + ) + db.add(report) + db.flush() + + # Findings + if privileged: + add_finding(db, case_id, "container", report.container_id, "HIGH", "Privileged container", f"{report.name} runs with --privileged") + if runs_as_root: + add_finding(db, case_id, "container", report.container_id, "MEDIUM", "Runs as root", f"{report.name} has no non-root user configured") + if host_network: + add_finding(db, case_id, "container", report.container_id, "MEDIUM", "Host networking", f"{report.name} uses host network") + if port_0_0_0_0: + add_finding(db, case_id, "container", report.container_id, "MEDIUM", "Public port exposure", f"{report.name} exposes ports on 0.0.0.0") + if added_caps: + add_finding(db, case_id, "container", report.container_id, "LOW", "Extra capabilities", f"Capabilities added: {report.added_caps}") + if mounts_str: + add_finding(db, case_id, "container", report.container_id, "LOW", "Mounted host paths", f"Mounts: {mounts_str}") + if suspicious_env: + add_finding(db, case_id, "container", report.container_id, "HIGH", "Suspicious env keys", f"Keys: {suspicious_env}") + +def analyze_images(client, db: Session, case_id: int): + now = datetime.datetime.utcnow() + for img in client.images.list(): + tags = img.tags or [] + repo_tags = ", ".join(tags) + latest_tag = any(t.endswith(":latest") for t in tags) + size_mb = round((getattr(img, 'attrs', {}).get('Size') or 0) / (1024*1024), 2) + created_str = getattr(img, 'attrs', {}).get('Created') + age_days = 0.0 + if created_str: + try: + created = datetime.datetime.fromisoformat(created_str.replace('Z','+00:00')) + age_days = (now - created).total_seconds() / 86400.0 + except Exception: + age_days = 0.0 + + report = ImageReport( + case_id=case_id, + image_id=img.short_id.replace("sha256:", ""), + repo_tags=repo_tags, + size_mb=size_mb, + latest_tag=latest_tag, + age_days=round(age_days, 1) + ) + db.add(report) + db.flush() + + if latest_tag: + add_finding(db, case_id, "image", report.image_id, "LOW", "Floating 'latest' tag", f"{repo_tags}") + if size_mb > IMAGE_SIZE_LIMIT_MB: + add_finding(db, case_id, "image", report.image_id, "LOW", "Large image size", f"{size_mb} MB > {IMAGE_SIZE_LIMIT_MB} MB threshold") + if age_days > IMAGE_AGE_LIMIT_DAYS: + add_finding(db, case_id, "image", report.image_id, "LOW", "Old image", f"{age_days:.0f} days old (> {IMAGE_AGE_LIMIT_DAYS})") + +def add_finding(db: Session, case_id: int, subject_type: str, subject_id: str, severity: str, title: str, detail: str): + f = Finding(case_id=case_id, subject_type=subject_type, subject_id=subject_id, severity=severity, title=title, detail=detail) + db.add(f) +``` + +### `python_service/app.py` + +```python +from fastapi import FastAPI +from pydantic import BaseModel +from typing import Optional +from database import SessionLocal +from models import Case, ContainerReport, ImageReport, Finding +from analyzer import analyze, init_db + +app = FastAPI(title="Docker Cyber Analyzer API", version="0.1.0") + +class AnalyzeRequest(BaseModel): + note: Optional[str] = None + +@app.on_event("startup") +def on_startup(): + init_db() + +@app.get("/healthz") +def healthz(): + return {"ok": True} + +@app.post("/analyze") +def run_analyze(req: AnalyzeRequest | None = None): + case_id = analyze() + if req and req.note: + with SessionLocal() as db: + c = db.get(Case, case_id) + if c: + c.note = req.note + db.commit() + return {"case_id": case_id} + +@app.get("/cases") +def get_cases(): + with SessionLocal() as db: + rows = db.query(Case).order_by(Case.id.desc()).all() + return [ + {"id": r.id, "created_at": r.created_at.isoformat(), "note": r.note} + for r in rows + ] + +@app.get("/cases/{case_id}") +def get_case(case_id: int): + with SessionLocal() as db: + c = db.get(Case, case_id) + if not c: + return {"error": "not found"} + containers = db.query(ContainerReport).filter_by(case_id=case_id).all() + images = db.query(ImageReport).filter_by(case_id=case_id).all() + findings = db.query(Finding).filter_by(case_id=case_id).all() + return { + "case": {"id": c.id, "created_at": c.created_at.isoformat(), "note": c.note}, + "containers": [{ + "id": r.id, "container_id": r.container_id, "name": r.name, "image": r.image, + "privileged": r.privileged, "runs_as_root": r.runs_as_root, "host_network": r.host_network, + "port_0_0_0_0": r.port_0_0_0_0, "added_caps": r.added_caps, "mounts": r.mounts, + "suspicious_env": r.suspicious_env + } for r in containers], + "images": [{ + "id": r.id, "image_id": r.image_id, "repo_tags": r.repo_tags, "size_mb": r.size_mb, + "latest_tag": r.latest_tag, "age_days": r.age_days + } for r in images], + "findings": [{ + "id": f.id, "subject_type": f.subject_type, "subject_id": f.subject_id, + "severity": f.severity, "title": f.title, "detail": f.detail + } for f in findings] + } +``` + +--- + +## Ruby dashboard (Sinatra) + +### `ruby_dashboard/Gemfile` + +```ruby +source 'https://rubygems.org' +gem 'sinatra' +gem 'thin' +gem 'sequel' +gem 'sqlite3' +``` + +### `ruby_dashboard/Dockerfile` + +```dockerfile +FROM ruby:3.2 + +WORKDIR /app +COPY Gemfile ./ +RUN bundle install + +COPY . . + +ENV RACK_ENV=production +ENV DB_PATH=/app/data/analyzer.db +CMD ["ruby", "app.rb", "-o", "0.0.0.0", "-p", "4567"] +``` + +### `ruby_dashboard/app.rb` + +```ruby +require 'sinatra' +require 'sequel' + +DB_PATH = ENV.fetch('DB_PATH', '/app/data/analyzer.db') +DB = Sequel.sqlite(DB_PATH) + +get '/' do + @cases = DB[:cases].order(Sequel.desc(:id)).all + erb :index +end + +get '/cases/:id' do + id = params[:id].to_i + @case = DB[:cases].where(id: id).first + halt 404, 'Not found' unless @case + @containers = DB[:container_reports].where(case_id: id).all + @images = DB[:image_reports].where(case_id: id).all + @findings = DB[:findings].where(case_id: id).all + erb :case +end + +__END__ + +@@ layout + + +
+ +Trigger a scan via API: curl -X POST http://localhost:8000/analyze
Created: <%= @case[:created_at] %> — Note: <%= @case[:note] %>
+| Severity | Subject | Title | Detail |
|---|---|---|---|
| <%= f[:severity] %> | +<%= f[:subject_type] %> <%= f[:subject_id] %> |
+ <%= f[:title] %> | +<%= f[:detail] %> | +
| ID | Name | Image | Privileged | Root | HostNet | PublicPorts | Caps | Mounts | Suspicious Env |
|---|---|---|---|---|---|---|---|---|---|
<%= c[:container_id] %> |
+ <%= c[:name] %> | +<%= c[:image] %> | +<%= c[:privileged] %> | +<%= c[:runs_as_root] %> | +<%= c[:host_network] %> | +<%= c[:port_0_0_0_0] %> | +<%= c[:added_caps] %> | +<%= c[:mounts] %> | +<%= c[:suspicious_env] %> | +
| ID | Repo Tags | Size (MB) | Latest? | Age (days) |
|---|---|---|---|---|
<%= i[:image_id] %> |
+ <%= i[:repo_tags] %> | +<%= i[:size_mb] %> | +<%= i[:latest_tag] %> | +<%= i[:age_days] %> | +