Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
63638f8
emit-init-run: auto-stamp ADO pipeline automationDetails from env + G…
michaelcfanning May 25, 2026
8b9152c
Port SARIF AI generation guidance from ai-plugins to sarif-sdk (#2930)
michaelcfanning May 25, 2026
7368311
Add PublishSampleToGhazdo.ps1 + clone-aware CweGenerateSample.ps1 (#2…
michaelcfanning May 25, 2026
90905cc
Scrub Microsoft-internal references from AI guidance port (#2932)
michaelcfanning May 26, 2026
1390256
Add multitool add-reporting-descriptor verb
michaelcfanning May 26, 2026
1764c8a
Strip editorial prefixes from AI notification taxonomy (#2934)
michaelcfanning May 26, 2026
68f147f
Generalize ALAS-SIGNAL notification id to LEARNING-SIGNAL (#2935)
michaelcfanning May 26, 2026
e0edb3d
Stamp ReleaseHistory UNRELEASED section as v5.0.0 (#2937)
michaelcfanning May 26, 2026
ac9c1d6
Trim and split the over-descriptive v5.0.0 notification-taxonomy BRK …
michaelcfanning May 26, 2026
979e16d
Fix CweGenerateSample.ps1 -GHAzDO crash inside real ADO pipelines (#2…
michaelcfanning May 26, 2026
21cbadd
Merge remote-tracking branch 'origin/main' into dev
michaelcfanning May 26, 2026
2c74427
Refresh v5.0.0 release-history layout + add prefix legend (#2939)
michaelcfanning May 26, 2026
be6fb70
Replace emit-init-run flags with SARIF Run JSON contract
michaelcfanning May 26, 2026
44b65f0
Stamp ReleaseHistory v5.0.1 section with nuget links
michaelcfanning May 26, 2026
62c9e80
Trim v5.0.1 release-notes bullets to neighbor density
michaelcfanning May 26, 2026
5225f82
Merge remote-tracking branch 'origin/main' into dev
michaelcfanning May 26, 2026
f471e8d
Add multitool add-invocation verb
michaelcfanning May 26, 2026
b1ca659
Drop unused System.Text using in EmitInitRunCommandTests
michaelcfanning May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions ReleaseHistory.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions docs/multitool-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ Use the SARIF Multitool to rewrite, enrich, filter, result match, and do other c
| rewrite | Transform a SARIF file to a reformatted version |
| suppress | Suppress results from a SARIF file |
| validate | Validate a SARIF File against the schema and against additional correctness rules. |
| emit-init-run | Open an append-only event log seeded with a SARIF run (driver identity, version control provenance, AI origin). |
| emit-init-run | Open an append-only event log seeded with a SARIF `run` JSON document (driver identity, version control provenance, AI origin) supplied via `--input` or stdin. |
| add-result | Append a fully-formed SARIF `result` object (JSON) to an in-progress event log. |
| add-notification | Append a fully-formed SARIF `notification` object (JSON) to an in-progress event log. |
| add-invocation | Append a fully-formed SARIF `invocation` object (JSON) to an in-progress event log. |
| emit-finalize | Replay a staged event log into a final SARIF file (with optional enrichment, embedding, and post-emit validation). |
| help | See Usage |
| version | Display version information |
Expand Down Expand Up @@ -74,8 +75,8 @@ Sarif.Multitool validate Other.sarif
: Validate against schema + AI profile rules (AI1003–AI2019)
Sarif.Multitool validate Other.sarif --rule-kind "Sarif;AI"

: Open an append-only event log for AI-produced findings (run skeleton only)
Sarif.Multitool emit-init-run my.sarif --tool-driver-name "MyScanner" --tool-driver-semantic-version 1.0.0 --ai-origin generated --vcp-repositoryuri https://github.com/org/repo --vcp-revisionid <sha> --vcp-branch main --srcroot file:///C:/repo
: Open an append-only event log for AI-produced findings (run header via stdin JSON)
'{"tool":{"driver":{"name":"MyScanner","semanticVersion":"1.0.0","informationUri":"https://myscanner.example.com/"}},"versionControlProvenance":[{"repositoryUri":"https://github.com/org/repo","revisionId":"<sha>","branch":"main","mappedTo":{"uriBaseId":"SRCROOT"}}],"originalUriBaseIds":{"SRCROOT":{"uri":"file:///C:/repo/"}},"automationDetails":{"guid":"a7ad9ab8-1234-5678-9abc-def012345678"},"properties":{"ai/origin":"generated"}}' | Sarif.Multitool emit-init-run my.sarif

: Append a result (JSON file form) to the in-progress event log
Sarif.Multitool add-result my.sarif --input result-001.json
Expand Down
73 changes: 57 additions & 16 deletions skills/emit-sarif-findings/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ metadata:
version: "1.0.0"
category: security
packages:
- "Sarif.Multitool >= 5.0.0"
- "Sarif.Multitool >= 5.0.1"
triggers:
- "emit SARIF"
- "write findings to SARIF"
Expand All @@ -32,30 +32,59 @@ Apply this skill when an agent is the **originating** detector (not post-process

## Prerequisites

- **`Sarif.Multitool` ≥ 5.0.0.** Recommended invocation: `dotnet dnx Sarif.Multitool --yes -- <verb> ...` (zero-install, version-resolved at first run; requires .NET 10+). Fall back to a global install with `dotnet tool install --global Sarif.Multitool` if `dotnet dnx` is unavailable.
- **`Sarif.Multitool` ≥ 5.0.1.** Recommended invocation: `dotnet dnx Sarif.Multitool --yes -- <verb> ...` (zero-install, version-resolved at first run; requires .NET 10+). Fall back to a global install with `dotnet tool install --global Sarif.Multitool` if `dotnet dnx` is unavailable.
- The current commit SHA, branch, repository URI, and a local source-root path.
- The normative profile doc: [`docs/ai/generating-sarif.md`](../../docs/ai/generating-sarif.md). Cross-reference it for every property you populate — do not invent vocabulary.

## Method

The skill uses four multitool verbs in sequence: `emit-init-run` → `add-result` (and/or `add-notification`) per finding → `emit-finalize --validate`. Each verb either appends to an event log (`<output>.wip.jsonl`) or replays the log into a finished SARIF file.
The skill uses five multitool verbs: `emit-init-run` → `add-result` / `add-notification` / `add-invocation` (per finding, event, or scan phase) → `emit-finalize --validate`. Each verb either appends to an event log (`<output>.wip.jsonl`) or replays the log into a finished SARIF file.

This staged design lets you build a run incrementally: hold one finding in working memory at a time, write it, move on. The final file is produced atomically by `emit-finalize`.

### Step 1 — Initialize the run

Construct a SARIF `Run` JSON object — the same partial-Run shape consumed by `SarifEventReplayer` — and pipe it to `emit-init-run`. The verb accepts the run header via `--input <path>` or stdin, exactly like `add-result` / `add-notification`. There is no flag-based form; if a field belongs on `run.*` in the final SARIF, place it on the JSON you supply here.

```powershell
dotnet dnx Sarif.Multitool --yes -- emit-init-run "{{OUTPUT_PATH}}" `
--tool-driver-name "{{SCANNER_NAME}}" `
--tool-driver-semantic-version "{{SCANNER_SEMVER}}" `
--information-uri "{{SCANNER_INFO_URI}}" `
--organization "{{ORGANIZATION}}" `
--ai-origin "{{AI_ORIGIN}}" `
--vcp-repositoryuri "{{REPO_URI}}" `
--vcp-revisionid "{{COMMIT_SHA}}" `
--vcp-branch "{{BRANCH}}" `
--srcroot "file:///{{LOCAL_SOURCE_ROOT}}" `
--automation-guid "{{NEW_GUID}}"
$runHeader = [ordered]@{
tool = @{
driver = [ordered]@{
name = "{{SCANNER_NAME}}"
semanticVersion = "{{SCANNER_SEMVER}}"
informationUri = "{{SCANNER_INFO_URI}}"
organization = "{{ORGANIZATION}}"
}
}
versionControlProvenance = @(
[ordered]@{
repositoryUri = "{{REPO_URI}}"
revisionId = "{{COMMIT_SHA}}"
branch = "{{BRANCH}}"
mappedTo = @{ uriBaseId = "SRCROOT" }
}
# Add more entries as needed — submodules, additional checkouts,
# cross-repo references. Attach a `properties` bag to any entry
# (e.g., `properties.skills = @("xss-detector", "sql-tainter")`) to
# document scanner/skill provenance for that source.
)
originalUriBaseIds = @{
SRCROOT = @{ uri = "file:///{{LOCAL_SOURCE_ROOT}}" }
}
automationDetails = [ordered]@{
guid = "{{NEW_GUID}}"
}
properties = @{
"ai/origin" = "{{AI_ORIGIN}}"
}
} | ConvertTo-Json -Depth 32

# Option A: pipe via stdin (matches add-result / add-notification).
$runHeader | dotnet dnx Sarif.Multitool --yes -- emit-init-run "{{OUTPUT_PATH}}"

# Option B: write to a file and reference it.
$runHeader | Set-Content run-header.json
dotnet dnx Sarif.Multitool --yes -- emit-init-run "{{OUTPUT_PATH}}" --input run-header.json
```

Inputs:
Expand All @@ -66,10 +95,14 @@ Inputs:
| `{{SCANNER_NAME}}` | yes | `run.tool.driver.name`. Keep stable across model upgrades — it is the producer identity. |
| `{{SCANNER_SEMVER}}` | yes | SemVer 2.0 string for `run.tool.driver.semanticVersion`. |
| `{{AI_ORIGIN}}` | yes | One of `generated`, `annotated`, `synthesized`. See `generating-sarif.md § AI Origin Declaration`. |
| `{{REPO_URI}}` / `{{COMMIT_SHA}}` / `{{BRANCH}}` | yes | Populates `run.versionControlProvenance[0]`. Required by rule AI1004. |
| `{{REPO_URI}}` / `{{COMMIT_SHA}}` / `{{BRANCH}}` | yes | Populates the first `run.versionControlProvenance` entry. Required by rule AI1004. Add additional entries — each with its own `properties` bag if useful — to document submodules, additional checkouts, or per-source scanner/skill provenance. |
| `{{LOCAL_SOURCE_ROOT}}` | yes for snippet/hash enrichment | A `file://` URI that the SDK can read to compute snippets and artifact hashes during `emit-finalize`. Rewritten to a portable URI in the finalize step. |
| `{{NEW_GUID}}` | yes | A fresh RFC 4122 GUID for `run.automationDetails.guid`. Required by rule AI2005. |

The verb validates a small set of profile-essential fields at receipt: `tool.driver.name` is required and must be a non-empty string; `tool.driver.informationUri` and `versionControlProvenance[].repositoryUri` must be `https`; `originalUriBaseIds["SRCROOT"].uri` must be `https` or `file`; GUIDs must be canonical 8-4-4-4-12 strings; `ai/origin` must be one of `generated`, `annotated`, `synthesized`. Anything else the SARIF schema accepts on a partial `Run` is appended to the `.wip.jsonl` run-header event unchanged; note that `emit-finalize` materializes a typed `SarifLog` from that event log, so fields outside the SDK's typed `Run` model are dropped at finalize. Durable custom data should live in SARIF `properties` bags, which the typed model preserves.

When the `TF_BUILD=True` environment indicates an Azure DevOps pipeline, `emit-init-run` stamps `automationDetails.id` plus the four `azuredevops/pipeline/build/*` properties required by GHAzDO ingestion. If your JSON supplies any of those fields, the values must match what the env detects, otherwise the verb fails with a conflict diagnostic — pick one source of truth.

### Step 2 — Append each result

For each finding, construct a complete SARIF `result` JSON object that conforms to `docs/ai/generating-sarif.md § Result Structure`, then append it:
Expand Down Expand Up @@ -97,6 +130,14 @@ Get-Content notification-001.json | dotnet dnx Sarif.Multitool --yes -- add-noti

See `docs/ai/generating-sarif.md § Execution Narrative & Configuration Feedback` for descriptor inventory and required shape.

### Step 3.5 — Append invocations (optional)

Use `add-invocation` to record one or more `Invocation` objects (`startTimeUtc`, `endTimeUtc`, `executionSuccessful`, `exitCode`, `commandLine`, `arguments`, `workingDirectory`, `environmentVariables`, properties bag, …). The replayer appends invocations to `run.invocations[]` in event order and attaches subsequent `add-notification` events to the most recent invocation, so emit a fresh invocation if you want to start a new notification group within the same scan.

```powershell
Get-Content invocation.json | dotnet dnx Sarif.Multitool --yes -- add-invocation "{{OUTPUT_PATH}}"
```

### Step 4 — Finalize and validate

```powershell
Expand Down Expand Up @@ -135,4 +176,4 @@ A complete reference SARIF file conforming to the AI profile is at [`docs/ai/exa

- **Multitool unavailable** — Install .NET 10+ for `dotnet dnx`, or `dotnet tool install --global Sarif.Multitool`. Do **not** attempt to hand-author SARIF JSON: the SDK's emit verbs handle enrichment, validation, and consistency in ways that are difficult to replicate by hand. If you genuinely have no .NET environment, the profile doc is the source of truth — read it carefully — but expect to invest significant effort to match SDK output.
- **`emit-finalize --validate` reports persistent errors** — Inspect the validation output (rule ID, message). Cross-reference the rule ID with `docs/ValidationRules.md` and the AI rule list in the profile doc. If a rule appears wrong (false positive against a correct construct), file an issue against the SDK — do not silence the rule.
- **Source root not available locally** — Drop `--srcroot` from `emit-init-run`. Snippets and artifact hashes will be empty; `--embed-text-files` will have no effect. The resulting log is still profile-conformant but less rich for consumers.
- **Source root not available locally** — Omit `originalUriBaseIds["SRCROOT"]` from the run header JSON and drop `--srcroot` from `emit-finalize`. Snippets and artifact hashes will be empty; `--embed-text-files` will have no effect. The resulting log is still profile-conformant but less rich for consumers.
75 changes: 75 additions & 0 deletions src/Sarif.Multitool.Library/Emit/AddInvocationCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Globalization;

using Microsoft.CodeAnalysis.Sarif.Driver;
using Microsoft.CodeAnalysis.Sarif.Emit;

using Newtonsoft.Json.Linq;

namespace Microsoft.CodeAnalysis.Sarif.Multitool
{
/// <summary>
/// Implements <c>multitool add-invocation</c>: appends a fully-formed SARIF invocation
/// JSON to <c>&lt;output&gt;.wip.jsonl</c>.
/// </summary>
/// <remarks>
/// <para>The verb performs no schema validation on the invocation payload beyond "must be
/// a JSON object" — SARIF §3.20 makes every field on <c>Invocation</c> optional, and AI
/// producers vary widely in which fields they have meaningful values for (a daemon may
/// know its <c>startTimeUtc</c> but not its <c>exitCode</c>; a one-shot scanner may know
/// both). Full-log validation belongs in <c>emit-finalize --validate</c>, not at receipt.</para>
/// <para>Invocations are replayed in event order to <c>run.invocations[]</c>. Subsequent
/// <c>execution-notification</c> and <c>configuration-notification</c> events attach to
/// the most recent invocation, so emitting a fresh invocation event MAY be used to start
/// a new notification group within the same scan.</para>
/// </remarks>
public class AddInvocationCommand : CommandBase
{
public int Run(AddInvocationOptions options, IFileSystem fileSystem = null)
{
fileSystem ??= Sarif.FileSystem.Instance;

try
{
int code = EmitEventLogHelpers.TryResolveWipPath(
options?.OutputFilePath,
fileSystem,
out string wipPath);
if (code != SUCCESS) { return code; }

code = EmitEventLogHelpers.TryReadJsonPayload(
options?.InputFilePath,
payloadKind: "invocation",
fileSystem,
out JToken payload);
if (code != SUCCESS) { return code; }

string executionSuccessful = payload["executionSuccessful"]?.Type == JTokenType.Boolean
? payload["executionSuccessful"].Value<bool>().ToString().ToLowerInvariant()
: "<unset>";

using (var writer = new SarifEventLogWriter(wipPath))
{
writer.Append(SarifEventKinds.Invocation, payload);
}

Console.Out.WriteLine(
string.Format(
CultureInfo.CurrentCulture,
"Appended invocation (executionSuccessful='{0}') to '{1}'.",
executionSuccessful,
wipPath));
return SUCCESS;
}
catch (Exception ex) when (!Debugger.IsAttached)
{
Console.Error.WriteLine(ex);
return FAILURE;
}
}
}
}
38 changes: 38 additions & 0 deletions src/Sarif.Multitool.Library/Emit/AddInvocationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using CommandLine;

namespace Microsoft.CodeAnalysis.Sarif.Multitool
{
/// <summary>
/// Options for <c>add-invocation</c>, which appends a fully-formed SARIF <c>invocation</c>
/// object to a staged event log (<c>&lt;output&gt;.wip.jsonl</c>) created by
/// <c>emit-init-run</c>.
/// </summary>
/// <remarks>
/// The invocation is supplied as a JSON document (file via <c>--input</c> or piped on
/// stdin). <see cref="SarifEventReplayer"/> strips any <c>invocations</c> array carried on
/// the run header — invocations must arrive as their own events — so this verb is the
/// only path a producer has to populate <c>run.invocations[]</c>. Subsequent
/// <c>add-notification</c> events attach to the most recent invocation in event order,
/// so producers MAY append additional invocations to start a new notification group
/// (e.g., to model a re-run within the same scan).
/// </remarks>
[Verb("add-invocation", HelpText = "Append a fully-formed SARIF invocation (JSON) to a staged event log.")]
public class AddInvocationOptions
{
[Value(
0,
MetaName = "<outputSarifPath>",
HelpText = "Path to the final SARIF file; the event log is appended to '<output>.wip.jsonl'.",
Required = true)]
public string OutputFilePath { get; set; }

[Option(
'i',
"input",
HelpText = "Path to a JSON file containing the SARIF invocation object. If omitted, JSON is read from stdin.")]
public string InputFilePath { get; set; }
}
}
Loading
Loading