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 CodeAnalyzer implements an agentic analysis of security defects in the source referenced by a given path.

  • A Critic invokes 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.