{
  "status": "SUCCESS",
  "scan_type": "ai_deep",
  "root": "/Users/jeevankumar/cyberr/CyberRant/Rant AI/npm-package/runtime",
  "files_audited": 14,
  "audited_files": [
    "patch_app.py",
    "agents/builtin_tools/file_recon.py",
    "persistence_models.py",
    "services/n8n_webhook.py",
    "cli/ui/diff_view.py",
    "agents/models.py",
    "cli/orchestration/tool_resolver.py",
    "agents/builtin_tools/git_secrets.py",
    "cli/config.py",
    "cli/auth.py",
    "cli/__init__.py",
    "cli/orchestration/__init__.py",
    "agents/builtin_tools/get_ip.py",
    "cli/version.py"
  ],
  "risk_level": "HIGH",
  "risk_score": 100,
  "grade": "F",
  "severity_counts": {
    "HIGH": 14,
    "MEDIUM": 28,
    "LOW": 17
  },
  "total_findings": 59,
  "findings": [
    {
      "file": "patch_app.py",
      "line": 29,
      "severity": "HIGH",
      "title": "Unconditional overwrite of source file without atomicity or backup",
      "detail": "The transformed content is written back to the same file with 'w' unconditionally at the end, even if none of the regex substitutions matched (partial or no-op patch). There is no backup, no temp-file-then-rename, and no verification that the output is valid Python. A crash mid-write, or a run that only partially applied the patch, corrupts app.py irreversibly. This is a real data-loss / broken-deploy risk.",
      "fix": "Write to a temp file, run ast.parse() to validate, then os.replace() atomically; also create a .bak backup and only write if a change actually occurred.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/file_recon.py",
      "line": 31,
      "severity": "HIGH",
      "title": "Purpose-built secret harvesting patterns",
      "detail": "The tool actively scans for and enumerates secret-bearing files (*.env, *secret*, *key*, *.pem, credential-laden *.json/*.yaml, docker-compose files). Combined with the default fallback to the entire current working directory, this recursively locates and inventories sensitive files anywhere the process can read, effectively producing a map of secrets for an attacker or a compromised agent.",
      "fix": "Restrict scanning to an explicitly allowlisted, non-sensitive directory; remove secret-oriented patterns; require an operator-supplied scope rather than defaulting to a broad recon of credentials.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/file_recon.py",
      "line": 44,
      "severity": "HIGH",
      "title": "Unbounded default search root falls back to CWD",
      "detail": "If AGENT_SANDBOX is unset, search_root becomes os.getcwd(), and os.walk descends the entire tree below it with no depth, size, or path confinement. In a service context the CWD can be the app root containing .env, keys, and config, so the tool exfiltrates their existence, paths, and sizes into an artifact and stdout.",
      "fix": "Fail closed when AGENT_SANDBOX is not set; validate that the configured root is a canonicalized path under an approved base directory before walking.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/file_recon.py",
      "line": 13,
      "severity": "HIGH",
      "title": "No path confinement / symlink traversal via os.walk",
      "detail": "os.walk follows directory symlinks are not followed by default, but symlinked files matched and os.stat'd resolve targets outside the intended root; more importantly there is no restriction keeping traversal inside AGENT_SANDBOX, and relpath does not prevent reading files outside via symlinks pointing outside the tree. An attacker who can plant a symlink inside the sandbox can cause the scanner to stat and report files anywhere the process can reach.",
      "fix": "Use os.walk(followlinks=False) explicitly, resolve each full_path with os.path.realpath and verify it stays within the canonicalized root before stat/report; skip symlinks.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 29,
      "severity": "HIGH",
      "title": "Insecure default webhook URL (plaintext HTTP)",
      "detail": "N8N_WEBHOOK_URL defaults to http://localhost:5678, plaintext HTTP. If misconfigured in production or partially set, telemetry including command output and community content is sent unencrypted, exposing sensitive data in transit. There is also no scheme/host validation on the env value.",
      "fix": "Require the env var explicitly (fail closed if unset in production), enforce https:// scheme, and validate the URL against an allow-list of expected hosts.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 76,
      "severity": "HIGH",
      "title": "SSRF / unvalidated base URL concatenation",
      "detail": "url is built via f-string concatenation of an operator-controlled env var (base_url) plus a fixed path, with no validation of the host, scheme, or resolved IP. If N8N_WEBHOOK_URL can be influenced (config injection, shared env, or an attacker-controlled deployment variable), httpx will POST internal telemetry to an arbitrary destination, enabling exfiltration or SSRF to internal metadata endpoints.",
      "fix": "Validate the resolved host against an allow-list, reject internal/link-local/loopback IP ranges, pin the scheme to https, and use httpx with redirects disabled.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 79,
      "severity": "HIGH",
      "title": "No authentication on outbound webhook",
      "detail": "POST requests to n8n carry sensitive execution telemetry (commands, outputs, community content, author identities) with no auth header, signature, or shared secret. Anyone able to reach the n8n endpoint or spoof it receives/accepts this data, and n8n has no way to verify the caller is the trusted backend.",
      "fix": "Add an HMAC signature header computed over the payload with a shared secret, or a bearer token / basic auth, and have n8n verify it before processing.",
      "source": "ai"
    },
    {
      "file": "agents/models.py",
      "line": 64,
      "severity": "HIGH",
      "title": "Guardrail bypass via trivial obfuscation / substring matching",
      "detail": "RED_LINE_PATTERNS and MALICIOUS_PATTERNS use literal substring matching against a lowercased prompt. An attacker can bypass every red-line by inserting punctuation, extra spaces, unicode homoglyphs, synonyms, or splitting words (e.g. 'shut  down all production servers', 'sh\u200butdown all production servers', 'delete every database', 'del all DBs'). The matching also has no normalization of whitespace/unicode, so the security control is effectively cosmetic and defeated by minor rephrasing.",
      "fix": "Do not rely on exact substring blocklists as a security boundary. Enforce authorization/capability checks at the action layer (e.g., a destructive operation like DB deletion must require explicit authenticated approval and RBAC), and use normalized (whitespace-collapsed, unicode-NFKC, homoglyph-folded) semantic/intent classification only as defense-in-depth, never as the sole gate.",
      "source": "ai"
    },
    {
      "file": "agents/models.py",
      "line": 55,
      "severity": "HIGH",
      "title": "Malicious-content check runs after mode selection but keyword precedence lets escalation triggers coexist with bypassed red-lines",
      "detail": "MODE_B_TRIGGERS include generic words like 'admin', 'report', 'logs', 'traffic'. Any prompt containing these auto-elevates to OPERATIONAL mode (line 55-56) even when the actual intent is malicious but phrased to avoid the exact MALICIOUS_PATTERNS strings. Combined with the substring-only red-line check, a crafted prompt can reach state COMPLETED in OPERATIONAL mode while performing a restricted intent, because classification is keyword-driven with no fail-closed default.",
      "fix": "Make governance fail closed: default to the most restrictive mode and require positive verification to escalate to OPERATIONAL; do not infer elevated operational capability from incidental keywords, and require authenticated authorization context to enter OPERATIONAL.",
      "source": "ai"
    },
    {
      "file": "cli/orchestration/tool_resolver.py",
      "line": 68,
      "severity": "HIGH",
      "title": "Dependency manifest path taken directly from untrusted artifacts map",
      "detail": "artifacts[name] is used as the command/path for a file_read task without any validation or containment to repo_root. If the artifacts dict (populated from prior scan output or user/context data) contains an absolute path or traversal like '../../etc/passwd', the resolver will emit a task reading arbitrary files outside the repo. There is no canonicalization or boundary check against repo_root.",
      "fix": "Resolve each artifact path against repo_root with os.path.realpath and verify the result stays within the realpath of repo_root before creating a file_read task; reject or skip paths that escape.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/git_secrets.py",
      "line": 94,
      "severity": "HIGH",
      "title": "Generic Credential group extraction bug leaks full match instead of captured value",
      "detail": "For the 'Generic Credential' pattern (grp=1), the condition `grp and m.lastindex and m.lastindex >= grp` gates using group(grp). If a match occurs but m.lastindex is falsy/None for some reason, it silently falls back to m.group(0), which includes the keyword prefix (e.g. 'password='), corrupting the dedup key and the placeholder check. More importantly, the PLACEHOLDER filter is then applied to the whole match rather than the secret value, so a benign key name near a real secret can suppress a genuine finding \u2014 a correctness/false-negative flaw for the highest-value generic detector.",
      "fix": "Always capture the intended group explicitly: use `val = m.group(grp) if grp else m.group(0)` and apply the placeholder test only to the captured secret value; do not rely on m.lastindex heuristics.",
      "source": "ai"
    },
    {
      "file": "cli/config.py",
      "line": 105,
      "severity": "HIGH",
      "title": "Fake encryption: token 'sealing' is trivially reversible XOR",
      "detail": "_seal/_unseal XOR the token against a key derived solely from public/local values (hostname, username, app name) via SHA256. This is not encryption \u2014 it provides no confidentiality. Any attacker who reads auth.json can recover the token by computing the same deterministic key (all inputs are recoverable from the file location and environment) or simply XOR/known-plaintext since base64 length and format labels ('sealed-v1') are stored alongside. This gives a false sense of security for stored bearer/refresh tokens.",
      "fix": "Do not roll custom obfuscation. Store secrets in the OS keychain (keyring), or if using a file, encrypt with a KDF-derived key from a user secret/passphrase (e.g., PBKDF2/scrypt/argon2 + AES-GCM). At minimum, document that the file is plaintext-equivalent and rely on file permissions.",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 391,
      "severity": "HIGH",
      "title": "OAuth callback server accepts token from any origin without state validation",
      "detail": "The local callback handler blindly trusts the `token`/`user`/`temp_token` query params on any GET request to 127.0.0.1:port. There is no validation of an OAuth `state`/nonce that was generated locally and echoed back. Any web page the user visits while the callback server is live (during the ~120s window) can issue a GET to http://127.0.0.1:<port>/?token=ATTACKER_TOKEN and the CLI will save the attacker's token as the user's credentials (login-CSRF / token fixation), causing the victim to operate under an account the attacker controls. The `client=cli:{port}` value is sent but never verified on return.",
      "fix": "Generate a cryptographically random state/nonce locally, pass it to the backend, and require the callback to echo it back; reject callbacks whose state does not match. Also validate Origin/Referer or use a one-time unguessable path, and only accept the first request.",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 563,
      "severity": "HIGH",
      "title": "Environment-variable auth bypass with static token 'local-trust'",
      "detail": "Setting RANTAI_LOCAL_TRUST=1 fabricates a fully authenticated session with a hardcoded token 'local-trust' and user_id 'local', bypassing all cloud sign-in. If any server-side or downstream component ever honors this session/token, or if the env var can be set in a shared/CI environment, it is a complete auth bypass. Insecure default reliance on an env flag for auth is dangerous.",
      "fix": "Ensure the 'local-trust' token is never accepted by any backend and that this mode strictly disables all cloud-authenticated operations; gate it behind an explicit build/dev flag not readable in production, and never persist it to the credentials file.",
      "source": "ai"
    },
    {
      "file": "patch_app.py",
      "line": 19,
      "severity": "MEDIUM",
      "title": "Non-idempotent / fragile regex-based code injection into source",
      "detail": "The patch inserts a class definition before 'class RantAIAgentCLI:' via re.sub with an unescaped pattern. The replacement string uses f-string interpolation of state_class which itself contains no regex backreferences, but combined with \\1 there is a real risk: if state_class ever contained a backslash or group-like sequence it would be interpreted in the replacement. More importantly, the guard on line 7 checks for 'class CLIState:' but re.sub on line 19 will do nothing (no error) if 'class RantAIAgentCLI:' is absent, leaving a partially-patched, syntactically inconsistent file that is then written back unconditionally.",
      "fix": "Verify the anchor pattern actually matched (use re.subn and assert count>0) before writing; use re.escape for literal patterns and pass replacement as a function or use string .replace() to avoid backreference interpretation.",
      "source": "ai"
    },
    {
      "file": "patch_app.py",
      "line": 22,
      "severity": "MEDIUM",
      "title": "Second patch anchored to 'self.verbose = False' may silently no-op or misfire",
      "detail": "The __init__ patch depends on the exact literal 'self.verbose = False' existing. If that line is absent, formatted differently, or appears in multiple places, the substitution either does nothing (leaving the CLIState class referencing state fields that are never initialized -> AttributeError at runtime) or injects duplicate/incorrect state initialization at the wrong indentation. The guard and the anchor are inconsistent, so the file can be written in a broken state.",
      "fix": "Use re.subn to confirm exactly one match; validate the resulting file parses (ast.parse) before writing back; fail loudly if the anchor is missing.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/file_recon.py",
      "line": 6,
      "severity": "MEDIUM",
      "title": "No input validation on root_dir or patterns",
      "detail": "root_dir and patterns are used unchecked. patterns is required to be iterable and each element a string for fnmatch; a non-string/None pattern raises inside the loop. root_dir is not validated to exist or be a directory, and there is no bound on tree size, allowing resource exhaustion on huge trees.",
      "fix": "Validate root_dir is an existing directory under an allowed base, ensure patterns is a list of strings, and enforce limits on number of files walked / recursion depth.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/file_recon.py",
      "line": 49,
      "severity": "MEDIUM",
      "title": "Artifact written to attacker-influenced current directory with no path control",
      "detail": "recon_files.json is written to the process CWD with a fixed name and no permission restriction, overwriting any existing file and creating a world-readable inventory of sensitive file locations.",
      "fix": "Write artifacts to a dedicated, access-controlled output directory with restrictive permissions (e.g., 0600) and a validated, non-overwriting path.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/file_recon.py",
      "line": 54,
      "severity": "MEDIUM",
      "title": "Sensitive recon results printed to stdout",
      "detail": "Paths, sizes, and modification times of secret/config files (including matched *secret*, *key*, *.pem, *.env) are emitted to stdout and to the JSON artifact, which typically ends up in logs, potentially leaking sensitive filesystem structure to less-trusted log sinks.",
      "fix": "Avoid emitting sensitive file inventories to stdout/logs; redact or gate output behind explicit authorization and store only in a protected location.",
      "source": "ai"
    },
    {
      "file": "persistence_models.py",
      "line": 23,
      "severity": "MEDIUM",
      "title": "ForeignKey lacks ON DELETE cascade at DB level",
      "detail": "The relationship on line 17 uses 'delete-orphan' cascade at the ORM level, but the ForeignKey on session_id has no ondelete='CASCADE'. If sessions are ever deleted outside the ORM (bulk delete, raw SQL, or a query.delete()), orphaned ChatMessage rows remain, and depending on DB constraints may cause integrity errors or leak conversation content that was intended to be deleted (data retention/privacy issue).",
      "fix": "Add ondelete='CASCADE' to the ForeignKey: ForeignKey('chat_sessions.id', ondelete='CASCADE') and ensure passive_deletes=True on the relationship.",
      "source": "ai"
    },
    {
      "file": "persistence_models.py",
      "line": 11,
      "severity": "MEDIUM",
      "title": "No tenant/ownership enforcement in model \u2014 access control must be enforced everywhere at query time",
      "detail": "user_id is a free-form String with no constraint linking a message/session to the querying user. Since access control is not modeled here, any endpoint that fetches by session id or message id without filtering on user_id enables IDOR (a user reading another user's conversations). The design invites broken access control because ownership is only implicit.",
      "fix": "Enforce ownership at every query (WHERE user_id = current_user), and consider a composite index/constraint; validate session ownership before returning ChatMessage rows.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 78,
      "severity": "MEDIUM",
      "title": "Redirects followed by default enabling SSRF pivot",
      "detail": "httpx.AsyncClient follows redirects behavior can be exploited: a compromised or malicious n8n host can 3xx-redirect the POST to an internal or external URL, turning a single outbound call into an SSRF pivot or leaking the (unauthenticated) payload elsewhere.",
      "fix": "Instantiate the client with follow_redirects=False and treat 3xx as an error.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 41,
      "severity": "MEDIUM",
      "title": "Sensitive command output forwarded without redaction",
      "detail": "Raw command and output (up to 5000 chars) are sent to an external service. These may contain secrets, tokens, PII, or internal paths captured during agent execution, and are then persisted by n8n. No sanitization/redaction is performed before egress.",
      "fix": "Redact secrets/PII from command and output before sending, and minimize the fields forwarded.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 82,
      "severity": "MEDIUM",
      "title": "Unbounded/untrusted response body parsed and returned",
      "detail": "response.json() is called on data from the n8n endpoint and returned to callers with no size limit or schema validation. A malicious or compromised n8n (or SSRF target) can return a huge or crafted body causing memory pressure or feeding untrusted structured data back into backend logic that may store it (e.g., community_intel/trend_history).",
      "fix": "Enforce a response size limit, validate content-type is application/json, and validate the parsed structure against an expected schema before using/returning it.",
      "source": "ai"
    },
    {
      "file": "cli/ui/diff_view.py",
      "line": 114,
      "severity": "MEDIUM",
      "title": "Rich markup injection via untrusted file paths in change summary",
      "detail": "render_change_summary uses Text.from_markup on a string built from ch.get('path') (and kind). If a proposed file path contains Rich markup syntax (e.g. '[/][link=file:///etc/passwd]...' or unbalanced tags), it will be interpreted as markup rather than literal text. Since paths originate from agent-proposed changes (attacker-influenced), an attacker can inject styling/link markup or corrupt the rendered review UI, potentially misleading the operator during the accept/reject security decision (e.g. hiding or spoofing which files are changed). This is a logic/UI-spoofing flaw a regex scan would miss.",
      "fix": "Do not pass untrusted content through from_markup. Build the Text object programmatically with Text.append(path, style=...) so the path is treated as literal text, or escape it with rich.markup.escape() before inclusion.",
      "source": "ai"
    },
    {
      "file": "cli/ui/diff_view.py",
      "line": 91,
      "severity": "MEDIUM",
      "title": "Untrusted path rendered as Rich markup in panel title",
      "detail": "render_file_diff builds the title with f-string interpolation of `path` and later Rich interprets markup in titles. A malicious/agent-supplied path containing '[', ']' or tag sequences can break out of the intended styling, spoof the displayed file name/action (NEW vs MODIFY), or inject link/style markup, misleading the operator's approval decision.",
      "fix": "Escape path with rich.markup.escape(path) before interpolation, or construct the title as a Text object with explicit styles instead of a markup string.",
      "source": "ai"
    },
    {
      "file": "agents/models.py",
      "line": 51,
      "severity": "MEDIUM",
      "title": "No input validation on prompt / agent_type",
      "detail": "classify_and_evaluate calls prompt.lower() without checking type or None; a None or non-string prompt raises AttributeError, and there is no length bound. An extremely large prompt causes repeated O(n*m) substring scans across all pattern lists (DoS), and unvalidated agent_type silently falls into OPERATIONAL default (line 61), potentially escalating privilege mode for unexpected agent identifiers.",
      "fix": "Validate that prompt is a non-empty string and enforce a maximum length before processing; validate agent_type against a known allowlist and fail closed (refuse) for unknown values rather than defaulting to OPERATIONAL.",
      "source": "ai"
    },
    {
      "file": "agents/models.py",
      "line": 24,
      "severity": "MEDIUM",
      "title": "Insecure default: system armed and secrets/config in source",
      "detail": "IS_SYSTEM_ARMED defaults to True and ACTIVE_PROMPT_VERSION is hardcoded in source, contradicting the comment that these 'should be managed via secure vault/env'. A default-armed system with in-code config means a deploy without proper config runs in an active/enabled state rather than fail-safe.",
      "fix": "Default IS_SYSTEM_ARMED to False (fail-safe) and load it plus prompt version from environment/secrets manager at startup, refusing to operate if configuration is absent.",
      "source": "ai"
    },
    {
      "file": "agents/models.py",
      "line": 83,
      "severity": "MEDIUM",
      "title": "Silent downgrade of state to COMPLETED for cross-mode requests",
      "detail": "When agent_type is ASK_RANT and mode resolves to OPERATIONAL, the function returns AgentState.COMPLETED regardless of the request's actual sensitivity, forcing LEARNING mode but marking success. This logic can mask operational/security-relevant requests as completed educational responses without any approval or audit gate, and there is no AWAITING_APPROVAL path anywhere for high-impact actions.",
      "fix": "Route sensitive operational requests through AWAITING_APPROVAL with an explicit human/authz step; never auto-COMPLETE a cross-mode operational request without logging and authorization.",
      "source": "ai"
    },
    {
      "file": "cli/orchestration/tool_resolver.py",
      "line": 15,
      "severity": "MEDIUM",
      "title": "repo_root/cwd used unvalidated as working_dir and search root",
      "detail": "repo_root is derived from context without validation and is passed as working_dir for shell git commands (line 86-89) and as the search root for file_search tasks. A caller-controlled context can set repo_root to any location on the filesystem, causing git and file searches to execute against arbitrary directories. Combined with the absence of an existence/containment check, this enables scanning/executing in unintended locations.",
      "fix": "Validate that repo_root is a non-empty, existing directory, canonicalize it, and constrain all downstream file operations and shell working_dir to that canonical path; fail fast if unset or invalid.",
      "source": "ai"
    },
    {
      "file": "cli/orchestration/tool_resolver.py",
      "line": 49,
      "severity": "MEDIUM",
      "title": "Signal-derived search patterns unvalidated (glob/path injection into file_search)",
      "detail": "signals are filtered only by containing '.' and then passed directly as file_search patterns with repo_root as the arg. A malicious signal such as '../*.pem' or an absolute-glob could redirect the search outside repo_root or be interpreted as a traversal pattern by the downstream search executor, leaking files beyond the intended scope.",
      "fix": "Sanitize/whitelist signal-derived patterns: reject patterns containing path separators or traversal sequences, or resolve globs strictly relative to and confined within repo_root.",
      "source": "ai"
    },
    {
      "file": "cli/orchestration/tool_resolver.py",
      "line": 111,
      "severity": "MEDIUM",
      "title": "Hardcoded network recon task with port-scan arguments",
      "detail": "network_analysis always emits a builtin_tool 'network_recon' with args ['127.0.0.1','22,80,443'], effectively scheduling a port scan. If the builtin's target host were ever sourced from context (or if this capability is triggered without authorization), this becomes an unauthenticated network probe primitive. The task is generated with no auth/authorization gating on who may request the capability.",
      "fix": "Gate network/socket/process capabilities behind explicit authorization checks and a strict allowlist of targets; do not hardcode scan ranges that could be repurposed, and require operator confirmation for MEDIUM+ risk network tasks.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/git_secrets.py",
      "line": 80,
      "severity": "MEDIUM",
      "title": "Commit-line parsing misattributes findings for merge/GPG commit lines",
      "detail": "`commit = line.split()[1]` blindly takes the second token of any line starting with 'commit '. Git log output can include lines like 'commit <hash> (HEAD -> main)' which is fine, but with --all and merges the diff hunk context or file content lines beginning with 'commit ' inside a patch body are also treated as new commit boundaries, causing findings to be attributed to the wrong commit/author. Since diff content lines are attacker-controllable (any committed file can contain 'commit deadbeef'), the reported 'commit' for rotation guidance can be spoofed/incorrect.",
      "fix": "Only treat a line as a commit header when it matches the strict format at a hunk boundary (e.g. regex `^commit [0-9a-f]{7,40}$`), or parse with `git log --format` sentinels / `-z` machine-readable output instead of scanning raw text.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/git_secrets.py",
      "line": 56,
      "severity": "MEDIUM",
      "title": "No path validation / arbitrary directory passed to git -C enabling scanning outside intended scope",
      "detail": "root is taken directly from argv and passed to `git -C root`. There is no confinement to an allowed base directory. When invoked as a builtin tool from CLI/desktop, a caller-controlled path lets the scanner run git in any repository on the host (e.g. another user's repo), and the JSON output (including author emails and masked-but-partially-revealing snippets) is returned. Combined with the artifact write below, this is a path/scope-control gap.",
      "fix": "Validate and canonicalize root, enforce it resides within an allowed workspace root (reject symlink escapes via os.path.realpath and prefix check), and reject paths outside the sandbox before invoking git.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/git_secrets.py",
      "line": 152,
      "severity": "MEDIUM",
      "title": "Report artifact written to attacker-influenced current working directory, overwriting files",
      "detail": "The report is written to a fixed relative filename 'git_secrets_report.json' in the process CWD without checking for existing files or symlinks. If CWD is attacker-controllable or the filename is pre-created as a symlink, this can clobber or redirect a write to an unintended location (symlink-follow). It also silently swallows OSError, hiding the failure. The report contains extracted secret snippets and author identities, so writing it to an unexpected/world-readable location is a secret-handling concern.",
      "fix": "Write the artifact to a controlled, validated output path (e.g. inside the scanned root or an explicit --out with path validation), open with O_CREAT|O_EXCL / os.O_NOFOLLOW to avoid symlink and overwrite attacks, and set restrictive file permissions (0600) since it holds secret material.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/git_secrets.py",
      "line": 107,
      "severity": "MEDIUM",
      "title": "Snippet masking can still leak short/high-entropy secret fragments",
      "detail": "The masking regex only replaces runs of 6+ chars from a limited charset. Secrets or fragments shorter than 6 chars, or characters outside [A-Za-z0-9/+=_-] (e.g. periods, colons inside JWT boundaries already handled, but values containing '.' segments), remain unmasked in the snippet that is printed to stdout and written to disk. For the Generic Credential value the whole line is masked but delimiters and short prefixes/suffixes of the secret can survive, partially disclosing credentials in the report.",
      "fix": "Do not include raw line content at all; emit only the detector name, file, and commit. If a snippet is required, redact the entire matched value explicitly by replacing the captured secret span with a fixed token rather than a length-thresholded charset regex.",
      "source": "ai"
    },
    {
      "file": "cli/config.py",
      "line": 174,
      "severity": "MEDIUM",
      "title": "Session file written without private permissions",
      "detail": "save_session writes SESSION_FILE which may contain sensitive session state, but never calls _ensure_private_permissions, so it inherits the default umask (often 0644), leaving it world-readable on shared systems. Only AUTH_FILE gets 0o600.",
      "fix": "Call _ensure_private_permissions(SESSION_FILE) after writing, and ideally set restrictive perms on CONFIG_DIR at creation (0o700).",
      "source": "ai"
    },
    {
      "file": "cli/config.py",
      "line": 192,
      "severity": "MEDIUM",
      "title": "History file written without private permissions and may capture secrets",
      "detail": "save_history persists up to 500 command-history entries in plaintext with default permissions. Command history for a CLI agent frequently contains tokens, API keys, or sensitive commands, yet the file gets no chmod 0o600 and no redaction.",
      "fix": "Apply _ensure_private_permissions(HISTORY_FILE) and redact known secret patterns before persisting history.",
      "source": "ai"
    },
    {
      "file": "cli/config.py",
      "line": 33,
      "severity": "MEDIUM",
      "title": "Config directory created with default (non-private) permissions",
      "detail": "_get_config_dir does mkdir with default mode. On multi-user systems the directory holding session/history/auth may be group/other-readable. Even where auth.json is chmod 600, a permissive parent dir can allow other attacks (e.g., replacing files, TOCTOU).",
      "fix": "Create the directory with mode 0o700 and verify/repair permissions on existing dirs (os.chmod).",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 428,
      "severity": "MEDIUM",
      "title": "User identity taken from unauthenticated redirect query param",
      "detail": "In the Google flow the `user` object (including id/email/username) is parsed directly from the `?user=` query parameter and stored as credentials without verifying it against the backend using the token. A malicious/forged callback can set arbitrary user_id/email while the real token belongs to someone else, or mismatch identity. The email/password flow re-fetches profile from /users/me but the Google flow trusts the query string.",
      "fix": "Ignore the `user` query param and always derive identity by calling fetch_profile(token) with the returned access token, as done in complete_device_login().",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 98,
      "severity": "MEDIUM",
      "title": "No client-side rate limiting on 2FA and password attempts / TOTP brute force window",
      "detail": "MAX_ATTEMPTS=3 limits local retries, but each interactive session resets the counter, and there is no backoff or lockout enforced client-side. Combined with a backend that may not rate-limit, repeated invocation of login_interactive/_complete_2fa allows unbounded guessing of passwords/TOTP codes. The code also strips spaces/dashes but does no length/format validation before sending.",
      "fix": "Enforce server-side rate limiting/lockout (primary), and add client-side exponential backoff plus TOTP format validation (exactly 6 digits) before sending.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/get_ip.py",
      "line": 11,
      "severity": "MEDIUM",
      "title": "Error message leakage via returned exception string",
      "detail": "On any failure the function returns f\"Failed to fetch external IP: {e}\", conflating an error string with a valid IP return value. Callers cannot distinguish a valid IP from an error, and the raw exception (which may include internal URL/proxy/network details or SSL error internals) is propagated to the caller/logs. This breaks the type contract (str IP vs error) and can leak environment info.",
      "fix": "Return a structured result (e.g. {'ok': False, 'error': 'network failure'}) or raise a well-defined exception; log the detailed exception internally rather than returning it. Never overload the success return value with an error string.",
      "source": "ai"
    },
    {
      "file": "patch_app.py",
      "line": 3,
      "severity": "LOW",
      "title": "Hardcoded relative path with no existence validation",
      "detail": "The script opens the target file using a hardcoded relative path 'npm-package/runtime/cli/app.py'. If run from an unexpected working directory this silently fails or, worse, could operate on an unintended file resolved via the relative path. There is no validation that the path exists or is the intended file before reading.",
      "fix": "Resolve the path relative to the script location (os.path.dirname(__file__)) and verify it exists and is the expected file before reading.",
      "source": "ai"
    },
    {
      "file": "persistence_models.py",
      "line": 25,
      "severity": "LOW",
      "title": "Unbounded content column length",
      "detail": "content is Column(String) with no length limit. On some backends String maps to a bounded VARCHAR and silently truncates or errors; on others it is unbounded, allowing storage-exhaustion via very large message payloads (no input validation/rate limiting present).",
      "fix": "Use Text for large content and enforce an explicit max size in the application layer before persisting.",
      "source": "ai"
    },
    {
      "file": "persistence_models.py",
      "line": 12,
      "severity": "LOW",
      "title": "agent_type accepts arbitrary strings despite documented enum",
      "detail": "Comment says 'STUDIO' or 'AGENT' but the column is an unconstrained String. Invalid values (e.g. logic-branching values) can be persisted, leading to inconsistent authorization/behavior downstream that branches on agent_type.",
      "fix": "Use SQLAlchemy Enum type or add a CHECK constraint and validate the value before insert.",
      "source": "ai"
    },
    {
      "file": "persistence_models.py",
      "line": 24,
      "severity": "LOW",
      "title": "role field unconstrained",
      "detail": "role is documented as 'user' or 'assistant' but unconstrained. If a downstream LLM pipeline trusts stored role to build prompts, an attacker able to write a message with a forged role (e.g. 'system') could enable prompt-injection/privilege escalation in the model context.",
      "fix": "Constrain role with an Enum/CHECK and never accept role from client input for assistant/system messages.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 81,
      "severity": "LOW",
      "title": "Logging may leak sensitive path/data context",
      "detail": "Log lines include the webhook path and error details; combined with debug-level context this can aid reconnaissance. Exception messages (line 90) may include URL/host details leaking internal topology into logs.",
      "fix": "Log minimal, non-sensitive context; avoid logging full URLs and raw exception content that may contain internal addresses.",
      "source": "ai"
    },
    {
      "file": "services/n8n_webhook.py",
      "line": 71,
      "severity": "LOW",
      "title": "No rate limiting / backpressure on outbound webhooks",
      "detail": "trigger_community_ingest and trigger_post_execution are invoked per event with no throttling or circuit breaker. A burst of community posts or executions can flood n8n and exhaust connections/threads on the backend, and a slow n8n plus 10s timeouts can pile up requests.",
      "fix": "Add a bounded queue/circuit breaker and rate limiting for outbound webhook calls; fire-and-forget via a background worker with concurrency limits.",
      "source": "ai"
    },
    {
      "file": "cli/ui/diff_view.py",
      "line": 90,
      "severity": "LOW",
      "title": "Diff body content not markup-escaped is safe but stats subtitle is fixed \u2014 no issue; note diff line content appended safely",
      "detail": "body.append() correctly treats diff line content as literal (no markup interpretation), so the diff body itself is not injectable. This is noted only to contrast with the title/subtitle/summary paths which do use markup interpretation.",
      "fix": "No change needed for body; ensure all markup-interpreted strings (title, subtitle, summary) escape untrusted input.",
      "source": "ai"
    },
    {
      "file": "cli/orchestration/tool_resolver.py",
      "line": 19,
      "severity": "LOW",
      "title": "No authorization or rate limiting on capability resolution",
      "detail": "resolve_tasks iterates arbitrary caller-supplied capability_ids and generates shell/network/secret-scanning tasks with no per-caller authorization, quota, or rate limiting. A caller can request every capability repeatedly, producing unbounded task volume including shell and network tasks.",
      "fix": "Enforce an authorization policy mapping callers to permitted capabilities and add rate/volume limits on task generation.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/git_secrets.py",
      "line": 67,
      "severity": "LOW",
      "title": "Unbounded git log output truncated silently causing missed history / DoS window",
      "detail": "The subprocess captures the full `git log -p --all` into memory with a 180s timeout, and only the first 400000 lines are scanned (line 78) with per-line truncation. On large repos this both risks large memory consumption before truncation and silently produces false-negatives (missed secrets beyond the cap) without indicating incompleteness to the caller, undermining the security guarantee the tool advertises.",
      "fix": "Stream git output line-by-line (Popen with iteration) instead of buffering stdout, and set a partial/incomplete flag in the result when the line cap or max_commits limit is hit so callers know the scan was truncated.",
      "source": "ai"
    },
    {
      "file": "cli/config.py",
      "line": 141,
      "severity": "LOW",
      "title": "Unhandled JSON/IO errors on credential/session/history load",
      "detail": "load_auth, load_session, load_history call json.load without try/except. A corrupted or attacker-tampered file raises an unhandled exception, potentially crashing the CLI or, in error paths, leaking file contents/stack traces. clear_auth also does not handle unlink errors.",
      "fix": "Wrap json.load in try/except returning {} / [] on failure, and log a safe warning; avoid printing raw file contents in errors.",
      "source": "ai"
    },
    {
      "file": "cli/config.py",
      "line": 54,
      "severity": "LOW",
      "title": "Agent API URL fully overridable via env with no validation (SSRF/misdirection risk)",
      "detail": "CYBERRANT_API_URL/CLOUD_BASE_URL/ACCOUNT_API_URL are taken directly from environment with no scheme or host validation. If any downstream code sends the auth token to CLOUD_BASE_URL/ACCOUNT_API_URL, an attacker who can influence the environment can redirect credentials to an arbitrary (http://, internal) endpoint. The comment even notes the agent endpoint can point to a local server.",
      "fix": "Validate that override URLs use https and, for the account/token endpoints, restrict to an allowlist of trusted hosts before sending bearer tokens; refuse plaintext http for credential-bearing requests.",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 447,
      "severity": "LOW",
      "title": "Callback token exposed in URL query string / server logs",
      "detail": "The access token arrives as a GET query parameter (handler.path query) which is logged by many HTTP intermediaries and browser history. Though log_message is suppressed here, the token in the URL is inherently leaky (browser history, referer). Combined with no TLS this is a secrets-handling weakness.",
      "fix": "Use POST with form body or a fragment-based flow so the token is not placed in the URL/query string; clear browser history is not possible so avoid query-string tokens entirely.",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 211,
      "severity": "LOW",
      "title": "Fragile/incorrect error-code parsing logic",
      "detail": "The expression on line 211-212 mixes ternary precedence: `body.get('code') or (body.get('message') or {}).get('code') if isinstance(...) else body.get('code')` evaluates ambiguously and can produce wrong codes, causing WRONG_PASSWORD to be misreported as USER_NOT_FOUND or vice versa, leaking account existence or confusing security decisions.",
      "fix": "Rewrite with explicit parentheses/branches to deterministically extract the error code, and avoid returning distinct USER_NOT_FOUND vs WRONG_PASSWORD messages that enable account enumeration.",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 226,
      "severity": "LOW",
      "title": "Account enumeration via distinct error messages",
      "detail": "Login returns 'Account doesn't exist' for USER_NOT_FOUND and 'Incorrect password' for WRONG_PASSWORD, allowing an attacker to enumerate valid accounts by observing which message is returned.",
      "fix": "Return a single generic 'Invalid email or password' message for both cases.",
      "source": "ai"
    },
    {
      "file": "cli/auth.py",
      "line": 112,
      "severity": "LOW",
      "title": "Backend base URL not validated (potential SSRF / token exfil via config)",
      "detail": "config.ACCOUNT_API_URL is used unvalidated to build every auth request including token exchange. If ACCOUNT_API_URL can be influenced (env/config file) it could point requests (and thus credentials/tokens) to an attacker-controlled host, exfiltrating passwords and tokens.",
      "fix": "Validate ACCOUNT_API_URL against an allowlist of trusted hosts and enforce https:// scheme before sending any credentials.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/get_ip.py",
      "line": 8,
      "severity": "LOW",
      "title": "Unvalidated external data used as trusted IP",
      "detail": "The 'ip' field from api.ipify.org is returned without any validation. A compromised/spoofed response (or MITM if TLS is stripped by a proxy) could inject arbitrary content that downstream code treats as a trusted IP address, enabling log injection or logic abuse where the value is used in security decisions.",
      "fix": "Validate the returned value with ipaddress.ip_address(data.get('ip')) before returning; reject non-IP responses.",
      "source": "ai"
    },
    {
      "file": "agents/builtin_tools/get_ip.py",
      "line": 7,
      "severity": "LOW",
      "title": "No response size limit on remote read",
      "detail": "response.read() reads the entire body with no cap. A malicious or misbehaving endpoint could return a very large body causing memory exhaustion (DoS) before JSON parsing fails.",
      "fix": "Read a bounded amount, e.g. response.read(4096), then parse; reject oversized responses.",
      "source": "ai"
    }
  ]
}