- API: add `POST /api/analyze_script` (app/blueprints/api.py)
- Fetch one external script to artifacts, run rules, return findings + snippet
- Uses new ExternalScriptFetcher (results_path aware) and job UUID
- Returns: { ok, final_url, status_code, bytes, truncated, sha256, artifact_path, findings[], snippet, snippet_len }
- TODO: document in openapi/openapi.yaml
- Fetcher: update `app/utils/external_fetch.py`
- Constructed with `results_path` (UUID dir); writes to `<results_path>/scripts/fetched/<index>.js`
- Loads settings via `get_settings()`, logs via std logging
- UI (results.html):
- Move “Analyze external script” action into **Content Snippet** column for external rows
- Clicking replaces button with `<details>` snippet, shows rule matches, and adds “open in viewer” link
- Robust fetch handler (checks JSON, shows errors); builds viewer URL from absolute artifact path
- Viewer:
- New route: `GET /view/artifact/<run_uuid>/<path:filename>` (app/blueprints/ui.py)
- New template: Monaco-based read-only code viewer (viewer.html)
- Removes SRI on loader to avoid integrity block; loads file via `raw_url` and detects language by extension
- Forms:
- Refactor `analyze_forms` to mirror scripts analysis:
- Uses rule engine (`category == "form"`) across regex/function rules
- Emits rows only when matches exist
- Includes `content_snippet`, `action`, `method`, `inputs`, `rules`
- Replace legacy plumbing (`flagged`, `flag_reasons`, `status`) in output
- Normalize form function rules to canonical returns `(bool, Optional[str])`:
- `form_action_missing`
- `form_http_on_https_page`
- `form_submits_to_different_host`
- Add minor hardening (lowercasing hosts, no-op actions, clearer reasons)
- CSS: add `.forms-table` to mirror `.scripts-table` (5 columns)
- Fixed table layout, widths per column, chip/snippet styling, responsive tweaks
- Misc:
- Fix “working outside app context” issue by avoiding `current_app` at import time (left storage logic inside routes)
- Add “View Source” link to open page source in viewer
Refs:
- Roadmap: mark “Source code viewer” done; keep TODO to add `/api/analyze_script` to OpenAPI
149 lines
5.4 KiB
Python
149 lines
5.4 KiB
Python
import os
|
||
import logging
|
||
from pathlib import Path
|
||
from flask import Flask
|
||
|
||
# Local imports
|
||
from .utils.settings import get_settings
|
||
from .utils.rules_engine import RuleEngine, load_rules_from_yaml, Rule
|
||
|
||
# our code based rules
|
||
from .rules.function_rules import (
|
||
FactAdapter,
|
||
FunctionRuleAdapter,
|
||
script_src_uses_data_or_blob,
|
||
script_src_has_dangerous_extension,
|
||
script_third_party_host,
|
||
form_submits_to_different_host,
|
||
form_http_on_https_page,
|
||
form_action_missing,
|
||
)
|
||
|
||
from app.blueprints import ui # ui blueprint
|
||
from app.blueprints import api # api blueprint
|
||
|
||
# from .utils import io_helpers # if need logging/setup later
|
||
# from .utils import cache_db # available for future injections
|
||
|
||
def create_app() -> Flask:
|
||
"""
|
||
Create and configure the Flask application instance.
|
||
|
||
Returns:
|
||
Flask: The configured Flask app.
|
||
"""
|
||
# Basic app object
|
||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||
|
||
# Load settings (safe fallback to defaults if file missing)
|
||
settings = get_settings()
|
||
|
||
# Secret key loaded from env (warn if missing)
|
||
app.secret_key = os.getenv("SECRET_KEY")
|
||
if not app.secret_key:
|
||
app.logger.warning("[init] SECRET_KEY is not set; sessions may be insecure in production.")
|
||
|
||
# Configure storage directory (bind-mount is still handled by sandbox.sh)
|
||
sandbox_storage_default = Path("/data")
|
||
app.config["SANDBOX_STORAGE"] = str(sandbox_storage_default)
|
||
|
||
# ---------------------------
|
||
# Suspicious Rules Engine
|
||
# ---------------------------
|
||
|
||
# Determine rules file path relative to this package (allow env override)
|
||
base_dir = Path(__file__).resolve().parent
|
||
default_rules_path = base_dir / "config" / "suspicious_rules.yaml"
|
||
rules_path_str = os.getenv("SNEAKYSCOPE_RULES_FILE", str(default_rules_path))
|
||
rules_path = Path(rules_path_str)
|
||
|
||
# Create engine bound to Flask logger so all verbose/debug goes to app.logger
|
||
engine = RuleEngine(rules=[], logger=app.logger)
|
||
|
||
# Try to load from YAML if present; log clearly if not
|
||
if rules_path.exists():
|
||
try:
|
||
loaded_rules = load_rules_from_yaml(rules_path, logger=app.logger)
|
||
# Add rules one-by-one (explicit, clearer logs if any rule fails to compile)
|
||
index = 0
|
||
total = len(loaded_rules)
|
||
while index < total:
|
||
engine.add_rule(loaded_rules[index])
|
||
index = index + 1
|
||
app.logger.info(f"[init] Loaded {len(loaded_rules)} suspicious rules from {rules_path}")
|
||
except Exception as e:
|
||
app.logger.warning(f"[init] Failed loading rules from {rules_path}: {e}")
|
||
else:
|
||
app.logger.warning(f"[init] Rules file not found at {rules_path}. Engine will start with zero rules.")
|
||
|
||
# Built-in function-based rules
|
||
adapter = FactAdapter(logger=app.logger)
|
||
|
||
engine.add_rule(Rule(
|
||
name="form_action_missing",
|
||
description="Form has no action attribute",
|
||
category="form",
|
||
rule_type="function",
|
||
function=FunctionRuleAdapter(form_action_missing, category="form", adapter=adapter),
|
||
))
|
||
|
||
engine.add_rule(Rule(
|
||
name="form_http_on_https_page",
|
||
description="Form submits via HTTP from HTTPS page",
|
||
category="form",
|
||
rule_type="function",
|
||
function=FunctionRuleAdapter(form_http_on_https_page, category="form", adapter=adapter),
|
||
))
|
||
|
||
engine.add_rule(Rule(
|
||
name="form_submits_to_different_host",
|
||
description="Form submits to a different host",
|
||
category="form",
|
||
rule_type="function",
|
||
function=FunctionRuleAdapter(form_submits_to_different_host, category="form", adapter=adapter),
|
||
))
|
||
|
||
# Script rules expect dict 'facts' (you’ll wire per-script facts later)
|
||
engine.add_rule(Rule(
|
||
name="script_src_uses_data_or_blob",
|
||
description="Script src uses data:/blob: URL",
|
||
category="script",
|
||
rule_type="function",
|
||
function=FunctionRuleAdapter(script_src_uses_data_or_blob, category="script", adapter=adapter),
|
||
))
|
||
|
||
engine.add_rule(Rule(
|
||
name="script_src_has_dangerous_extension",
|
||
description="External script with dangerous extension",
|
||
category="script",
|
||
rule_type="function",
|
||
function=FunctionRuleAdapter(script_src_has_dangerous_extension, category="script", adapter=adapter),
|
||
))
|
||
|
||
engine.add_rule(Rule(
|
||
name="script_third_party_host",
|
||
description="Script is from a third-party host",
|
||
category="script",
|
||
rule_type="function",
|
||
function=FunctionRuleAdapter(script_third_party_host, category="script", adapter=adapter),
|
||
))
|
||
|
||
# Store engine both ways: attribute (convenient) and config
|
||
app.rule_engine = engine
|
||
app.config["RULE_ENGINE"] = engine
|
||
|
||
# App metadata available to templates
|
||
app.config["APP_NAME"] = settings.app.name
|
||
app.config["APP_VERSION"] = f"v{settings.app.version_major}.{settings.app.version_minor}"
|
||
|
||
# Register blueprints
|
||
app.register_blueprint(ui.bp)
|
||
app.register_blueprint(api.api_bp)
|
||
|
||
# Example log lines so we know we booted cleanly
|
||
app.logger.info(f"SneakyScope started: {app.config['APP_NAME']} {app.config['APP_VERSION']}")
|
||
app.logger.info(f"SANDBOX_STORAGE: {app.config['SANDBOX_STORAGE']}")
|
||
app.logger.info(f"Registered {len(engine.rules)} total rules (YAML + function)")
|
||
|
||
return app
|