Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ src/Sarif/Taxonomies/CweTaxonomy.brief.md text eol=lf
# Pinned: this resource is embedded and hashed; line-ending drift would change the hash.
src/Test.UnitTests.Sarif/TestData/InsertOptionalDataVisitor/InsertOptionalDataVisitor.txt text eol=crlf

# Pinned: rule message strings are compiled into embedded resources and reproduced
# verbatim in functional-test baselines; LF drift would rewrite every multi-line
# message and break those baselines.
src/Sarif.Multitool.Library/Rules/RuleResources.resx text eol=crlf

###############################################################################
# Set default behavior for command prompt diff.
#
Expand Down
3 changes: 3 additions & 0 deletions ReleaseHistory.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Each release entry below is prefixed with one of:

Entries are terse by design: one line per change, present-tense behavior, complete but only essential data. No issue/PR archaeology or narrative — that history lives in the engineering system.

## **v5.4.3** [Sdk](https://www.nuget.org/packages/Sarif.Sdk/v5.4.3) | [Driver](https://www.nuget.org/packages/Sarif.Driver/v5.4.3) | [Converters](https://www.nuget.org/packages/Sarif.Converters/v5.4.3) | [Multitool](https://www.nuget.org/packages/Sarif.Multitool/v5.4.3) | [Multitool Library](https://www.nuget.org/packages/Sarif.Multitool.Library/v5.4.3)
* BUG: `GHAzDO1019.ProvidePipelineProperties`, the `emit-run` pipeline-context producer, and the `ai-sarif-log.schema.json` build contract accept `-1` as the buildDefinitionId sentinel for runs with no saved definition, still rejecting `0` and other negatives.

## **v5.4.2** [Sdk](https://www.nuget.org/packages/Sarif.Sdk/v5.4.2) | [Driver](https://www.nuget.org/packages/Sarif.Driver/v5.4.2) | [Converters](https://www.nuget.org/packages/Sarif.Converters/v5.4.2) | [Multitool](https://www.nuget.org/packages/Sarif.Multitool/v5.4.2) | [Multitool Library](https://www.nuget.org/packages/Sarif.Multitool.Library/v5.4.2)
* BUG: `SARIF2012.ProvideRuleProperties` exempts `NOVEL-` rules from the missing-`helpUri` note; a novel finding has no catalog topic to cite.
* BUG: `emit-invocations` stops stamping a receipt-time `endTimeUtc` default and rejects any `startTimeUtc`, `endTimeUtc`, or notification `timeUtc` more than five minutes past the emit clock.
Expand Down
19 changes: 14 additions & 5 deletions src/Sarif.Multitool.Library/Emit/AdoPipelineContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ public static DetectionState TryDetect(
}

int buildDefinitionIdValue = 0;
if (TryParseRequiredPositiveInt(buildDefIdPrimary, buildDefIdFallback, BuildDefinitionIdPrimaryEnvVar, BuildDefinitionIdFallbackEnvVar, present, problems, out int parsedBuildDefId))
if (TryParseRequiredPositiveInt(buildDefIdPrimary, buildDefIdFallback, BuildDefinitionIdPrimaryEnvVar, BuildDefinitionIdFallbackEnvVar, present, problems, allowNegativeOneSentinel: true, out int parsedBuildDefId))
{
buildDefinitionIdValue = parsedBuildDefId;
}
Expand All @@ -175,7 +175,7 @@ public static DetectionState TryDetect(
}

int buildIdValue = 0;
if (TryParseRequiredPositiveInt(buildId, null, BuildIdEnvVar, null, present, problems, out int parsedBuildId))
if (TryParseRequiredPositiveInt(buildId, null, BuildIdEnvVar, null, present, problems, allowNegativeOneSentinel: false, out int parsedBuildId))
{
buildIdValue = parsedBuildId;
}
Expand Down Expand Up @@ -525,7 +525,7 @@ private static bool TryParseRequiredGuid(string raw, string envName, List<string
return true;
}

private static bool TryParseRequiredPositiveInt(string primaryRaw, string fallbackRaw, string primaryEnvName, string fallbackEnvName, List<string> present, List<string> problems, out int value)
private static bool TryParseRequiredPositiveInt(string primaryRaw, string fallbackRaw, string primaryEnvName, string fallbackEnvName, List<string> present, List<string> problems, bool allowNegativeOneSentinel, out int value)
{
value = 0;

Expand All @@ -547,9 +547,18 @@ private static bool TryParseRequiredPositiveInt(string primaryRaw, string fallba
return false;
}

if (parsed <= 0)
// A valid build definition id is any positive integer, or the -1 sentinel ADO uses
// for runs not associated with a saved definition. Zero and any other negative value
// are rejected. Other pipeline ids (e.g. buildId) remain strictly positive.
if (parsed <= 0 && !(allowNegativeOneSentinel && parsed == -1))
{
problems.Add(string.Format(CultureInfo.InvariantCulture, "{0}='{1}' must be a positive integer", sourceEnv, raw));
problems.Add(string.Format(
CultureInfo.InvariantCulture,
allowNegativeOneSentinel
? "{0}='{1}' must be a positive integer or the -1 sentinel"
: "{0}='{1}' must be a positive integer",
sourceEnv,
raw));
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,9 @@
],
"properties": {
"azuredevops/pipeline/build/buildDefinitionId": {
"description": "Integer != 0, serialized as a string.",
"description": "Positive integer, or the -1 sentinel, serialized as a string.",
"type": "string",
"pattern": "^-?(?!0+$)[0-9]+$"
"pattern": "^(-1|[1-9][0-9]*)$"
},
"azuredevops/pipeline/build/buildDefinitionName": {
"description": "Non-empty, non-whitespace string.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.Sarif.Multitool.Rules
/// ingestion to drop the run with "SarifValidation_MissingAdoPipelineProperties".
///
/// Required keys (all under run.automationDetails.properties):
/// azuredevops/pipeline/build/buildDefinitionId (int, != 0)
/// azuredevops/pipeline/build/buildDefinitionId (int; positive or the -1 sentinel)
/// azuredevops/pipeline/build/buildDefinitionName (non-empty string)
/// azuredevops/pipeline/build/phaseId (GUID, != Guid.Empty)
/// azuredevops/pipeline/build/phaseName (non-empty string)
Expand Down Expand Up @@ -77,7 +77,8 @@ protected override void Analyze(Run run, string runPointer)
nameof(RuleResources.GHAzDO1019_ProvidePipelineProperties_Error_MissingBuildDefinitionId_Text),
BuildDefinitionIdKey);
}
else if (!int.TryParse(buildDefinitionIdValue, out int buildDefinitionId) || buildDefinitionId == 0)
else if (!int.TryParse(buildDefinitionIdValue, out int buildDefinitionId)
|| (buildDefinitionId <= 0 && buildDefinitionId != -1))
{
LogResult(
automationDetailsPointer,
Expand Down
2 changes: 1 addition & 1 deletion src/Sarif.Multitool.Library/Rules/RuleResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ Required keys on 'run.automationDetails.properties':
<value>{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition.</value>
</data>
<data name="GHAzDO1019_ProvidePipelineProperties_Error_InvalidBuildDefinitionId_Text" xml:space="preserve">
<value>{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id.</value>
<value>{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition.</value>
</data>
<data name="GHAzDO1019_ProvidePipelineProperties_Error_MissingBuildDefinitionName_Text" xml:space="preserve">
<value>{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition."
},
"Error_InvalidBuildDefinitionId": {
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a non-zero integer. The {1} service requires a non-zero integer build definition id."
"text": "{0}: The 'automationDetails.properties' value for '{2}' is '{3}', which is not a positive integer or the -1 sentinel. The {1} service requires a positive build definition id, or the -1 sentinel for runs not associated with a saved definition."
},
"Error_MissingBuildDefinitionName": {
"text": "{0}: The 'automationDetails.properties' bag does not provide '{2}'. This property is required by the {1} service to identify the build definition by name."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void TryDetect_OneRequiredVarMissing_ReturnsPartialAndNamesIt()
[Fact]
public void TryDetect_RejectsMalformedOrZeroBuildDefinitionId()
{
// Both non-integer values and the literal "0" must fail the positive-int guard
// Both non-integer values and the literal "0" must fail the build-definition-id guard
// (zero is invalid per GHAzDO1019).
FakeEnvironmentVariableGetter badInt = CompleteEnv();
badInt.With(AdoPipelineContext.BuildDefinitionIdPrimaryEnvVar, "abc");
Expand All @@ -110,6 +110,46 @@ public void TryDetect_RejectsMalformedOrZeroBuildDefinitionId()
err2.Should().Contain("positive");
}

[Fact]
public void TryDetect_AcceptsNegativeOneSentinelBuildDefinitionId()
{
// -1 is ADO's sentinel for a run not associated with a saved build definition.
FakeEnvironmentVariableGetter env = CompleteEnv();
env.With(AdoPipelineContext.BuildDefinitionIdPrimaryEnvVar, "-1");
env.With(AdoPipelineContext.BuildDefinitionIdFallbackEnvVar, "-1");

AdoPipelineContext.TryDetect(env, out AdoPipelineContext ctx, out string error)
.Should().Be(AdoPipelineContext.DetectionState.Complete);
error.Should().BeNull();
ctx.BuildDefinitionId.Should().Be(-1);
}

[Fact]
public void TryDetect_RejectsNegativeNonSentinelBuildDefinitionId()
{
FakeEnvironmentVariableGetter env = CompleteEnv();
env.With(AdoPipelineContext.BuildDefinitionIdPrimaryEnvVar, "-2");
env.With(AdoPipelineContext.BuildDefinitionIdFallbackEnvVar, "-2");

AdoPipelineContext.TryDetect(env, out _, out string error)
.Should().Be(AdoPipelineContext.DetectionState.Partial);
error.Should().Contain(AdoPipelineContext.BuildDefinitionIdPrimaryEnvVar);
error.Should().Contain("-1 sentinel");
}

[Fact]
public void TryDetect_RejectsNegativeOneBuildId()
{
// The -1 sentinel allowance is specific to buildDefinitionId; buildId stays strictly positive.
FakeEnvironmentVariableGetter env = CompleteEnv();
env.With(AdoPipelineContext.BuildIdEnvVar, "-1");

AdoPipelineContext.TryDetect(env, out _, out string error)
.Should().Be(AdoPipelineContext.DetectionState.Partial);
error.Should().Contain(AdoPipelineContext.BuildIdEnvVar);
error.Should().Contain("positive");
}

[Fact]
public void TryDetect_RejectsMalformedOrEmptyTeamProjectId()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,21 @@ void Expect(string label, JsonObject automationDetails, bool accept)
((JsonObject)missingProp["properties"]).Remove("azuredevops/pipeline/build/phaseName");
Expect("pipeline-missing-phaseName", missingProp, false);

// buildDefinitionId == "0": GHAzDO1019 rejects (int != 0).
// buildDefinitionId == "0": GHAzDO1019 rejects (positive or -1 sentinel only).
JsonObject zeroId = CompliantPipeline();
zeroId["properties"]["azuredevops/pipeline/build/buildDefinitionId"] = "0";
Expect("pipeline-zero-buildDefinitionId", zeroId, false);

// buildDefinitionId == "-1": accepted (ADO sentinel for runs with no saved definition).
JsonObject sentinelId = CompliantPipeline();
sentinelId["properties"]["azuredevops/pipeline/build/buildDefinitionId"] = "-1";
Expect("pipeline-negative-one-buildDefinitionId", sentinelId, true);

// buildDefinitionId == "-2": GHAzDO1019 rejects (negative, non-sentinel).
JsonObject negativeId = CompliantPipeline();
negativeId["properties"]["azuredevops/pipeline/build/buildDefinitionId"] = "-2";
Expect("pipeline-negative-buildDefinitionId", negativeId, false);

// phaseId == empty GUID: GHAzDO1019 rejects (Guid != Empty).
JsonObject emptyPhase = CompliantPipeline();
emptyPhase["properties"]["azuredevops/pipeline/build/phaseId"] = "00000000-0000-0000-0000-000000000000";
Expand Down
Loading
Loading