Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@

All notable changes to CyberAI are documented here.

## [0.5.0] - 2026-06-18
### Differentiated Platform — Week 4
Week 4 gives CyberAI its unique edge: out-of-band-driven exploitation, a
Web3 audit track, an MCP server, report self-validation, bug-bounty scope
import, and a web dashboard.

### Added
- OOB-driven exploitation: phantom-grid v2.0 client (token-flow), payload
library v2 (7 categories), `OOBWorkflow` + `ExploitAgentOOB` correlating
injected payloads against live callbacks.
- Nuclei exploit engine: subprocess wrapper with JSONL parsing, searchsploit
integration (graceful), CVE→OOB heuristic for JNDI/SSRF templates.
- Web3 track: standalone `SmartContractAgent`, Slither wrapper, Immunefi
severity classifier (per-check table + impact×confidence fallback).
- MCP server: official `mcp` SDK, recon + intel tools exposed as MCP tools
with JSON Schema and graceful dispatch (Claude Desktop / Cursor docs).
- LLM-as-Judge: `judge_report` cross-checks report claims against KB
evidence, `JudgeVerdict`, feedback-driven retry, per-finding confidence.
- Bug-bounty scope import: HackerOne / Bugcrowd JSON → in/out scope with
exclusion-aware matching (`!host` overrides allow-wildcards).
- Web dashboard: FastAPI backend reading sessions from disk, SSE live phase
progress, single-file htmx + alpinejs UI (no build step).

### Changed
- Web backend migrated from dead Flask stubs to FastAPI; sessions are now
read from disk (single source of truth shared with `cyberai replay`).

## [0.4.0] - 2026-06-12

### Accelerated & Observable — Week 3
Expand Down
2 changes: 1 addition & 1 deletion cyberai/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = "0.4.0"
__version__ = "0.5.0"
__author__ = "evkir"
__description__ = "CyberAI — AI-native multi-agent pentest platform"
61 changes: 38 additions & 23 deletions cyberai/web/app.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
"""
CyberAI Flask API server.
REST interface for starting scans, querying sessions, serving reports.
CyberAI FastAPI server.

REST + SSE interface for listing scan sessions and serving reports.
Sessions are read from disk (config.output_dir / session_<id>.json),
the same artifacts the CLI `replay` command consumes.
"""

from flask import Flask, jsonify
from cyberai.web.routes.session import session_bp
from cyberai.web.routes.report import report_bp
from __future__ import annotations

import logging
from pathlib import Path

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from cyberai.core.config import CyberAIConfig
from cyberai.web.routes.report import router as report_router
from cyberai.web.routes.session import router as session_router

logger = logging.getLogger("cyberai.web")

_TEMPLATES = Path(__file__).parent / "templates"

def create_app() -> Flask:
app = Flask(__name__)
app.config["JSON_SORT_KEYS"] = False

# Register blueprints
app.register_blueprint(session_bp, url_prefix="/api")
app.register_blueprint(report_bp, url_prefix="/api")
def create_app(config: CyberAIConfig | None = None) -> FastAPI:
"""Build the FastAPI app. Pass a config to override the sessions dir."""
app = FastAPI(title="CyberAI API", version="0.5.0")
app.state.config = config or CyberAIConfig()

@app.get("/health")
def health():
return jsonify({"status": "ok", "service": "CyberAI API"})
app.include_router(session_router, prefix="/api")
app.include_router(report_router, prefix="/api")

@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "not found"}), 404
@app.get("/health")
def health() -> dict:
return {"status": "ok", "service": "CyberAI API"}

@app.errorhandler(500)
def server_error(e):
return jsonify({"error": "internal server error"}), 500
@app.get("/", response_class=HTMLResponse)
def dashboard() -> str:
index = _TEMPLATES / "dashboard.html"
if not index.exists():
return "<h1>CyberAI</h1><p>dashboard.html missing</p>"
return index.read_text()

logger.info("CyberAI API server created")
logger.info("CyberAI FastAPI app created")
return app


app = create_app()


if __name__ == "__main__":
app = create_app()
app.run(host="127.0.0.1", port=8888, debug=False)
import uvicorn

uvicorn.run("cyberai.web.app:app", host="127.0.0.1", port=8888, reload=False)
111 changes: 61 additions & 50 deletions cyberai/web/routes/report.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,65 @@
"""
/api/report — serve generated markdown/JSON reports.
/api/sessions/{id}/report — serve a session's generated markdown report.

The report path is resolved from the session's knowledge base
(report.markdown_path), written by ReportAgent. Falls back to a 404-shaped
error dict when a session has no report (e.g. dry-run scans).
"""

from flask import Blueprint, jsonify, send_file, abort
from __future__ import annotations

import json
from pathlib import Path
import os

report_bp = Blueprint("report", __name__)

REPORTS_DIR = Path(os.getenv("CYBERAI_REPORTS_DIR", "reports/"))


@report_bp.get("/report")
def list_reports():
"""GET /api/report — list available report files."""
if not REPORTS_DIR.exists():
return jsonify({"reports": [], "count": 0})

files = [
{
"filename": f.name,
"size_bytes": f.stat().st_size,
"modified": f.stat().st_mtime,
}
for f in REPORTS_DIR.iterdir()
if f.suffix in (".md", ".json", ".html", ".pdf")
]
files.sort(key=lambda x: x["modified"], reverse=True)
return jsonify({"reports": files, "count": len(files)})


@report_bp.get("/report/<filename>")
def get_report(filename: str):
"""
GET /api/report/<filename>
Serves the report file. Sanitizes path to prevent traversal.
"""
# Path traversal guard
safe_name = Path(filename).name
report_path = REPORTS_DIR / safe_name

if not report_path.exists():
abort(404)

suffix = report_path.suffix.lower()
mime_map = {
".md": "text/markdown",
".json": "application/json",
".html": "text/html",
".pdf": "application/pdf",
}
mimetype = mime_map.get(suffix, "application/octet-stream")
return send_file(report_path, mimetype=mimetype)

from fastapi import APIRouter, Request
from fastapi.responses import PlainTextResponse

router = APIRouter()


def _sessions_dir(request: Request) -> Path:
return Path(request.app.state.config.output_dir)


def _report_path_for(session: dict) -> str | None:
kb = session.get("kb")
if isinstance(kb, dict):
val = kb.get("report.markdown_path")
if isinstance(val, dict): # kb entries may wrap value+meta
val = val.get("value")
if isinstance(val, str):
return val
return None


@router.get("/sessions/{session_id}/report", response_class=PlainTextResponse)
def get_report(session_id: str, request: Request):
"""Return the markdown report body for a session, or an error dict."""
out = _sessions_dir(request)
safe = Path(session_id).name
spath = out / f"session_{safe}.json"
if not spath.exists():
return PlainTextResponse(
json.dumps({"error": "session not found"}),
status_code=404,
media_type="application/json",
)
session = json.loads(spath.read_text())

md = _report_path_for(session)
# Guard traversal: report must live inside the sessions dir.
if md:
rp = Path(md)
if rp.name == rp.as_posix().split("/")[-1] and rp.exists():
try:
rp.resolve().relative_to(out.resolve())
except ValueError:
md = None
else:
return PlainTextResponse(rp.read_text(), media_type="text/markdown")

return PlainTextResponse(
json.dumps({"error": "no report for session", "session_id": session_id}),
status_code=404,
media_type="application/json",
)
Loading
Loading