Skip to content

Add findings to the report - #80

Open
naftali-g wants to merge 5 commits into
masterfrom
naftali/findings-report
Open

Add findings to the report#80
naftali-g wants to merge 5 commits into
masterfrom
naftali/findings-report

Conversation

@naftali-g

Copy link
Copy Markdown
Contributor

No description provided.

@naftali-g
naftali-g requested a review from jtoman July 16, 2026 13:43
Comment thread composer/spec/source/report/collect.py Outdated
class EvidenceFetcher(Protocol):
"""Backend hook: given a violated rule's run link + name, return its `RuleEvidence` (or ``None``
when the backend has none). Prover reads the run-scoped CEX-analysis capture; foundry has none."""
async def __call__(self, link: str | None, rule_name: str, /) -> "RuleEvidence | None":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

absolutely no reason to quote this type annotation. a little curious you have the "positional only" marker here but I'm not mad about it.

Comment thread composer/spec/source/report/findings.py Outdated
For each violated rule (a `RuleVerdict` with ``outcome == Outcome.BAD``) this reshapes the
counterexample analysis the run already produced — looked up via the backend `EvidenceFetcher` — into
a `Finding`. One structured LLM call per violation, fed the *distilled* analysis rather than the raw
counterexample, so the expensive counterexample reasoning is not repeated. Best-effort per finding:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what expensive reasoning? the cex analysis in cex_analysis_raw or whatever it is is single shot llm call?

Comment thread composer/spec/source/report/findings.py Outdated
counterexample, so the expensive counterexample reasoning is not repeated. Best-effort per finding:
any failure drops that one finding, never the report.

v1 scope: prover-only (a foundry BAD is an author-declared demonstration, not a discovered bug).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I... don't think this is true? if you write an invariant test or a fuzz test, it's worth an explanation of why that invariant failure happened or why the fuzz test parameters chosen were problematic right? But for v1 this is fine, but I just want to make sure we don't buy this reasoning and avoid revisiting this for foundry in the future.

Comment thread composer/spec/source/report/findings.py Outdated
Comment on lines +11 to +13
system template. ``IssueIn.locations`` is not produced here — a run knows only local paths and CVL-spec
lines, so the submission layer reconstructs source locations from the engagement scope + counterexample
(the accurate report-time locator is on ``FindingProvenance``: rule name, spec file, prover-run link).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure I'll understand this more as I read the rest of the file, but having read it without further context, this last sentence is unintelligible.

Comment thread composer/spec/source/report/findings.py Outdated
max_length=4000,
description="1-3 sentences: the Impact tier, the Likelihood tier, and the matrix cell they select.",
)
summary: str = Field(max_length=2000, description="1-3 sentence tl;dr.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"tl;dr" really?

prover_runs: list[RunView]
rule_counts: list[ChipView]
group_counts: list[ChipView]
findings: list[FindingView]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

Comment thread composer/spec/source/report/schema.py Outdated
# and the ``content`` prose. One finding is emitted per violated rule (a `RuleVerdict` with
# ``outcome == Outcome.BAD``); its prose is synthesized from the rule's counterexample analysis and
# the property/group it breaks (see ``report/findings.py``). Sherlock length caps are mirrored but
# not enforced (report.json is lenient — the submission endpoint is the validating gate).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh THIS is where the length caps come from? But ... wait what API are we targeting here?

Comment thread composer/spec/source/report/schema.py Outdated
Comment on lines +178 to +183
summary: str = Field(max_length=2000, description="Short tl;dr of the finding, in one to three sentences.")
description: str = Field(max_length=50000, description="Full technical description of the vulnerability and how it manifests.")
impact: str = Field(max_length=20000, description="Concrete consequence if exploited (funds at risk, DoS, data exposure, ...).")
attack_path: str | None = Field(default=None, max_length=20000, description="Step-by-step exploit path, if applicable.")
assumptions_and_uncertainties: str | None = Field(default=None, max_length=10000, description="Assumptions relied on and anything the submitter is unsure about.")
proof_of_concept: str | None = Field(default=None, max_length=65536, description="Executable PoC, or the prover counterexample demonstrating the issue.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks super similar to the fields found in the findings.py FindingDraft schema, common basemodel class?

Comment thread composer/spec/source/cex_capture.py Outdated
Comment on lines +9 to +11
In-memory is sufficient: the report phase runs in the same process as formalization within
``composer.pipeline.core.run_pipeline``. A ``BaseStore``-backed variant (cf.
``composer.prover.report_store``) would be the resume-safe upgrade.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly suspect we'll someday want to use the agentic handler for the cex in autoprover, so make sure we're not baking in an assumption about always using the TrivialFanoutCexHandler here.

That said, why not use the BaseStore version? seems like we'd want to save this sort of thing?

Comment on lines +43 to +46
Use `informational` ONLY for a property break with no real-world exploit path — a code-quality or
specification observation, not a vulnerability. A prover counterexample proves the property CAN be
broken, so never downgrade a genuine break to `informational` just because it looks hard to reach;
instead lower the Likelihood tier and let the matrix decide.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we expect informational to never be the correct output, why even include it in the output schema?

This also suggests we shouldn't let the agent choose this, but pick the two axes on this matrix?

@naftali-g
naftali-g requested a review from jtoman August 2, 2026 09:54

@jtoman jtoman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of this looks much, much better. One major question about the grouping/granularity of saving/restoring CEX. Sorry I didn't notice it before...

# which one actually broke — that is the LLM's job, not ours to pre-guess.
props = props_by_ref.get(rule.ref, [])
# A rule's properties may share a group (some may be in none); dedupe by slug, first-seen order.
rule_groups = list({g.slug: g for p in props if (g := group_by_key.get(p.key)) is not None}.values())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neat trick

props = props_by_ref.get(rule.ref, [])
# A rule's properties may share a group (some may be in none); dedupe by slug, first-seen order.
rule_groups = list({g.slug: g for p in props if (g := group_by_key.get(p.key)) is not None}.values())
user = load_jinja_template(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strong preference for you to move this our TypedTemplate mechanism, it will let you automatically get template fuzz testing (among other things).

attack_path=draft.attack_path,
assumptions_and_uncertainties=draft.assumptions_and_uncertainties,
proof_of_concept=_trim(ev.counterexample) if ev else None,
**{f: getattr(draft, f) for f in AuthoredContent.model_fields},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what on god's green earth???

**AuthoredContent.model_dump(draft) would also work (probably), but I'm not sure its that much more readable/less gross. Whatever.

Comment on lines +40 to +44
await self.store.aput(
self.namespace,
rule_name,
CexAnalysis(analysis=analysis, counterexample=counterexample).model_dump(mode="json"),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait. I didn't notice this in the last round: this keys on the path.rule. But for a parametric rule like:

run whatever(method f) { ... }

that's just whatever. The f instantiation is part of the RulePath method (or contract if it exists too). Keying on rule alone might be what you want, but it means the last CEX within any rule gets stored, and all the others get silently overwritten...

# final-iteration analysis to the report's findings synthesizer. None -> the finding degrades
# to property/group text.
rec = self._analysis_store.get(rule_name)
# final-iteration analysis to the findings synthesizer. None -> the finding degrades to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto here; do we want one finding per rule? What if a rule is broken for different reasons across different instantiations?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants