Project
PRISM Scout — passive security recon tool
Session type
Bug investigation → feature design
Outcome
Spec written, committed to repo
Redactions
Target domain & service names removed for privacy
Phase 1 — Investigation
You

Please read the PRISM Scout report and see the critical warning about API. I tried independently verifying if this is an actual exposure or not. I tried using developer tools to locate the file, but I can't find that either. Can you independently verify the finding is true, or a false positive?

↳ Read 1 file ↳ Fetch cdn.<redacted>/agent/static/…/<redacted>.js 533.7 KB · 200 OK ↳ Fetch https://app.<redacted> 3.7 KB · 200 OK

The critical finding is in <redacted>.js from cdn.<redacted>. Let me fetch that file to verify what's actually there.


The <redacted>.js file itself has no hardcoded credentials — that's expected. But the scanner found the apiKey somewhere in it. The real question is whether this is a subscription/agent key (which is intentionally public) or something else. Let me check how it's initialized on the actual site.


This is a false positive. Here's the full picture:

The "exposed credential" is the <redacted> subscription key (agent key). It's called in the site's JS as:

})('<redacted-uuid>');

This is intentionally public by design:

  1. It's not a secret — the <redacted> agent/subscription key is analogous to a Google Analytics tracking ID. It identifies which account to send behavioral analytics to. The vendor's own docs expect it to live in client-side JS.
  2. It's already in the CDN URL — the same GUID appears in the script's URL path, so it's doubly public just from the <script> tag.
  3. It can't access the management API — the REST API (which does expose sensitive analytics data) requires a separate Integration API key generated in the vendor UI. The agent key alone grants no read/write access to the account.

The [REDACTED] in the report is PRISM Scout masking the value for display, not something the site is hiding. The value is fully visible on the page.

What this means for the scanner: the secret detector is triggering on the apiKey variable name pattern in the vendor's initialization snippet and classifying it as a generic secret. It should be tuned to recognize these subscription key patterns (UUID-format values passed to third-party loader functions) and downgrade or suppress them. Worth filing as a false-positive reduction task.
Phase 2 — Brainstorming the Fix
You

Can you research ways for the tool to verify false positives for something like apiKey? Maybe it gets sent to the LLM to independently verify before deciding if it is concern-worthy?

⚡ Skill: superpowers:brainstorming ↳ Read 4 files

Good context gathered. I now understand the full pipeline well.

The Generic Secret Var pattern is the culprit — it's a broad catch-all (apiKey = "...") that inevitably hits public/intentional keys. The more specific patterns (AWS AKIA prefix, OpenAI sk-, Stripe sk_live_) are rarely false positives.


Question 1 of 5

Should false-positive verification apply broadly to all secret patterns, or only to the noisy catch-all (Generic Secret Var)?

My instinct is to scope it narrowly — patterns like AWS Access Key, OpenAI API Key, GitHub Token are almost never wrong, while Generic Secret Var is inherently fuzzy. Targeting just the fuzzy pattern keeps the change focused.

You

Targeting the fuzzy patterns sounds like the smart move in this case.

Question 2 of 5

Should false-positive verification work even when no LLM is configured?

Right now, if LLM_BACKEND isn't set, Phase 5 scoring is skipped entirely. We could build a purely deterministic layer (CDN allowlists, UUID detection, variable name analysis) that always runs, and optionally layer LLM verification on top. Or we could make it LLM-only and just skip verification when no LLM is available.

You

For now, LLM only. No unnecessary complexity, and I'll always tie in a local LLM.

Question 3 of 5

Where in the pipeline should verification happen? There are two natural spots:

  • At detection time (inside secret_detector.run()) — each fuzzy match gets verified before it becomes a Finding. The scanner itself becomes "smart" and self-filters.
  • As a new Phase between analysis and scoring — secret findings accumulate normally, then a dedicated verification step reviews fuzzy ones before Phase 5 scoring sees them.
You

Let's add a verification step. As I continue to test, I will find more things that could benefit from it.

Question 4 of 5

What should happen to a finding the LLM determines is a false positive — should it be dropped entirely from the report, or kept but downgraded/flagged?

You

If the verification concludes it's purely intentional, it could maybe include it as part of a 'tech stack' finding, but should not count towards an increased vulnerability score.

Question 5 of 5

Should the LLM verify fuzzy findings one at a time, or in a single batch call?

Batching is faster and cheaper, but per-finding calls give the LLM more focused context and makes it easier to add per-finding reasoning to the output.

You

Let's go 'per-finding'. We want to be good, not fast.

Phase 3 — Approach Selection

Perfect. I have everything I need. Here are three approaches:

Option B — Verification as a new scanner in the registry

Add a verifier scanner that runs in Phase 2 alongside the other analysis scanners, but only after secret_detector completes. It mutates findings in ctx.findings directly.

Trade-offs: Fits the existing pattern but requires serial ordering within Phase 2 (which is currently fully parallel). Mutation of shared context mid-phase is fragile.
Option C — Fold verification into the Phase 5 LLM scoring prompt

Extend RISK_SCORE_PROMPT to also flag findings it considers false positives and suggest reclassifications.

Trade-offs: Zero new infrastructure, but mixes two responsibilities into one prompt (scoring vs. verification). Per-finding focus is lost since all findings land in one call.

My recommendation is Option A. It keeps the LLM agent layer cohesive, the verification logic is isolated and testable, and FUZZY_PATTERNS gives you a clean extension point as you find more patterns that need this treatment.

You

Option A, I agree with you here. The separate verification layer will be useful later, I just know it.

Phase 4 — Design Review

Design — Section 1: Architecture & Data Flow

The pipeline gains a new Phase 4.5: Secret Verification between subscan deduplication and LLM scoring:

Phase 4 → deduplication → Phase 4.5 → secret verification → Phase 5 → LLM risk scoring

secret_detector.py exports a FUZZY_PATTERNS set — initially just {"Generic Secret Var"}. Any finding whose title contains one of these pattern names is a candidate for verification.

The new prism_scout/agent/verifier.py exposes one public coroutine:

async def verify_secret_finding(finding: Finding, llm: LLMClient) -> Finding

It sends a focused prompt to the LLM with the finding's pattern name, masked value, file URL, and context snippet. The LLM returns structured JSON:

{
  "is_intentional_public_key": true,
  "service_name": "<redacted> Analytics",
  "reasoning": "..."
}

If is_intentional_public_key is true, the verifier reclassifies the finding in-place: category → TECH, severity → INFO, title becomes "Third-Party Tracking: <service>". If false, the finding is returned unchanged (remains CRITICAL).

The orchestrator loops through fuzzy findings sequentially, skips verification if llm.is_available is False, and logs each decision.

You

This approach looks perfect.

Design — Section 2: Verification Prompt

A new SECRET_VERIFICATION_PROMPT in prompts.py. The LLM receives all the context needed to make a confident judgment:

You are reviewing a potential credential exposure found in
client-side JavaScript.

Finding details:
- Pattern matched: {pattern_name}
- Code context: {context}
- File URL: {file_path}
- Masked value: {masked_value}

Determine whether this is an intentionally public client-side
key or a genuinely sensitive credential that should not be
exposed.

Intentionally public keys are designed to be in client-side
code — examples: [vendor] agent keys, Google Analytics IDs,
Mixpanel project tokens, Segment write keys, Intercom app IDs.

Genuinely sensitive credentials include: secret API keys,
service account tokens, database passwords, private keys.

Output JSON only, no prose:
{
  "is_intentional_public_key": <true|false>,
  "service_name": "<service name, or null>",
  "reasoning": "<one sentence>"
}

The context field passes the code snippet that secret_detector already captures — giving the LLM the variable name and surrounding code.

You

This will be fine. I'll be using a local LLM, so exposure isn't really a concern. I just need the LLM to know where to look and what to verify.

Noted — and since it's a local LLM we can pass the unmasked context snippet for better accuracy. The prompt just needs to be clear and directive, not defensive about exposure.


Design — Section 3: Extension Point

FUZZY_PATTERNS in secret_detector.py is the single control surface for routing findings to verification:

# Any pattern name listed here is routed through Phase 4.5.
FUZZY_PATTERNS: set[str] = {
    "Generic Secret Var",
}

Any future pattern added here automatically gets the verification treatment in Phase 4.5 — no orchestrator changes needed.

One deliberate constraint: verification is skipped when the LLM is unavailable. The finding stays CRITICAL rather than being silently dropped — better to over-report than to miss a real credential because the LLM wasn't configured.

That covers the full design. Ready to write the spec doc and move to implementation planning?

You

Let's do it.

Outcome
↳ Write docs/superpowers/specs/2026-03-26-secret-verification-phase-design.md ↳ git commit aad752f

Spec written and committed to docs/superpowers/specs/2026-03-26-secret-verification-phase-design.md. Please review it and let me know if you want to make any changes before we start writing out the implementation plan.