Design#
pw_fortifier is designed to provide libraries that can be used to build
agentic scanning tools. These libraries are built on a common framework that
implements the scanner as a pipeline of individual stages.
Goals#
The overall goal of building an agentic scanning framework results in some immediate sub-goals for the design:
Flexibility: The space of agentic tooling is evolving rapidly. The best agents, skills, and models for a particular task may be supplanted by new ones in the coming days, weeks, and months. As a result, it is strongly undesirable to tie the scanner framework to any specific approach. Instead, the pipeline design seeks to identify the sequence of steps required to perform a scan regardless of implementation, and provide clear injection points where tool owners can add or swap agents, skills, and models as needed.
Performance: Scanning an entire codebase can be arduous. Strictly sequencing calls to various agents and subprocesses can lead to bottlenecks that make frequent scanning infeasible. The pipeline design heavily relies on an asynchronous execution model to ensure stages that can make progress are not blocked by those that are waiting on agent or subprocess responses.
Extensibility: While these tools were originally envisioned for upstream Pigweed, it is clear they may have value to downstream consumers as well. To maximize this value, the pipeline design makes as few assumptions about project details as possible, and provides specific interfaces to add functionality to stages like the Analyzer or code editors to handle cases beyond those encountered in upstream.
Reusability: Finally, it is desirable to have a common mechanism for multiple use cases. Upstream Pigweed in particular has two identified types of issues they wish to scan for: security defects and out-of-date third-party packages. The pipeline design allows the underlying framework to be reused for each of these workflows.
High-level design#
The Scanner class orchestrates an
asynchronous, multi-stage data processing pipeline.
PipelineStages are asynchronous tasks
that run in a loop, and may be producers, consumers, or both.
Each consumer
reads from an asyncio.Queue and processes the input. Consumers are resilient
against transient errors like network timeouts, and will automatically retry
processing (up to a configured
maximum). They routinely
save their intermediate state within the
working directory, allowing
interrupted runs to be
resumed.
Each producer
writes to the input queue of the next stage. When there are no more outputs to
be produced, producers send a sentinel value that consumers recognize as
indicating the queue is closed.
Many stages are both consumers and producers. They read inputs, process and transform them, and then send them on to the next stage. On receiving a closure sentinel, they send the sentinel to the next stage and stop running.
Finally, the module also includes the
PipelineMux and
PipelineDemux classes. These allow
“fanning out” based on input name, and then “fanning in” again back to a single
next stage.
Pipeline architecture#
The pipeline coordinates discovering, analyzing, triaging, filing, and patching issues. The diagram below illustrates the flow of data through the standard pipeline stages:
flowchart TD
subgraph "Discovery & Generation"
A[Emitter] --> B[Analyzer Stages]
B --> C[Deduplicator]
end
subgraph "Triage & Tracking"
C --> D[Triager]
D --> E[IssueWriter]
E --> F[Issue Demux]
G[IssueReader] --> F
end
subgraph "Action & Output"
F --> H[Editor]
H --> I[Collector]
end
Pipeline stages#
The Scanner pipeline consists of the following
common stages:
Emitter#
The Emitter stage is the entry point when
scanning files. It uses a PathEnumerator to walk the
read-only repository and
discover files matching registered glob patterns, emitting file paths to
downstream stages.
Alternatively, when specific files are passed via the command line, only those matching files are emitted.
Finally, if specific issues or
hotlists are provided via the
command line, the Emitter is disabled entirely. The provided issues will be
passed by the IssueReader stage instead.
Analyzer#
This is a collection of stages that transforms paths to files into specific findings. The number of stages and how they are related to one another are specific to the tool implementation.
Code analysis for DefectScanner#
For example, the DefectScanner class
requires two types be
provided for code analysis:
A
CodeAnalyzerimplements an agentic analysis of security defects in the source referenced by a given path.A
Criticinvokes an agent to challenge the previous step’s findings and validate they are legitimate defects.
Package analysis for FreshnessScanner#
As another example, the
FreshnessScanner class uses a
PipelineMux and
PipelineDemux to send paths to
PackageAnalyzers that have been
registered with the
scanner.
Deduplicator#
The pw_fortifier.deduplicator.Deduplicator stage filters out
duplicate findings by cross-referencing candidate items against historical
issues and existing bug tracker state.
The implementation of this type is correlated with the IssueWriter stage. Depending on how predictable the output of the latter is, the implementation of this stage may range from completely deterministic to fully agentic.
Triager#
The Triager class determines the severity of
an issue, and uses
CoreOwnerFinder to
select an assignee.
Scanner-specific implementations will produce scanner-specific issues. For
example, DefectScanner produces
Defects, while
FreshnessScanner produces
FreshnessResults.
IssueWriter#
The pw_fortifier.issue_tracker.IssueWriter class is used to create
Buganizer issues. When
-b / –create-bugs is
enabled, it creates new issues via an
pw_fortifier.issue_tracker.IssueTracker implementation that handles
details such as component IDs, default hotlists, and specific Buganizer APIs.
When -v / –verbose is enabled, it prints issue titles and descriptions to stdout.
IssueReader#
In normal bulk-scanning mode, the Emitter puts
paths into the pipeline to be processed by the preceding stages. However, the
user can instead specify issues
or hotlists to bypass those
stages. In this case, the IssueReader
fetches existing issues directly from the issue tracker and injects them into
the pipeline directly upstream of the code editor. This facilitates creating CLs
for existing issues.
Code Editor#
Much like the Analyzer stages, the code editor stage implementation varies between tools while sharing a single purpose: to create Gerrit CLs that resolve the discovered issues.
Unlike the Analyzer stages, all code changes are handled by a single stage.
This stage is also the only one that interacts with the
writable repository. This
avoids any concurrent reads or writes of files when the code editor is making
changes.
In the absence of –allow-uploads, the code editors will not publish their CLs to Gerrit and preserve them locally.
PoC and fix generator for DefectScanner#
The code editor for the DefectScanner
is an implementation of the
PocAndFixGenerator. As the name
implies, this code editor attempts to make two sets of changes:
First, it attempts to add a unit test that acts as a “proof of concept” (PoC) for the security defect. A valid PoC fails the test if the defect could be exploited.
Next, it attempts to fix the vulnerability. If a valid PoC was created, that test should pass with the fix applied.
This code editor will create CLs containing the PoCs and fixes for _each_ issue and publish them to the Gerrit instance associated with the repository (when –allow-uploads is enabled).
Roll generator for FreshnessScanner#
The code editor for the
FreshnessScanner is the
RollGenerator class. This class
has a default behavior of simply trying to revise the version of a third-party
package and verifying that upstream Pigweed still builds, passes its unit tests,
and passes its presubmit checks. It also allows for registering additional
PackageUpdater instances that handle
editing code and verifying builds for specific packages or files.
This code editor will create a _single_ CL containing all the rolls that were successfully applied and publish it to the Gerrit instance associated with the repository (when –allow-uploads is enabled).
Collector#
The final Collector stage is a “sink” that
collects processed items, prints formatted console summaries, and optionally
writes structured CSV reports to an
output file.