Skip to content
Open
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
13 changes: 1 addition & 12 deletions src/Sarif.Multitool.Library/MergeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@ public class MergeCommand : CommandBase
private readonly List<string> _toolKeyOrder;
private readonly Dictionary<string, Run> _toolKeyToMergedRun;
private readonly Dictionary<string, RunMergingVisitor> _toolKeyToVisitor;
private readonly Dictionary<string, HashSet<Result>> _toolKeyToResults;

public MergeCommand(IFileSystem fileSystem = null) : base(fileSystem)
{
_toolKeyOrder = new List<string>();
_toolKeyToMergedRun = new Dictionary<string, Run>();
_toolKeyToVisitor = new Dictionary<string, RunMergingVisitor>();
_toolKeyToResults = new Dictionary<string, HashSet<Result>>();
}

public int Run(MergeOptions mergeOptions)
Expand Down Expand Up @@ -163,7 +161,7 @@ private async Task<bool> MergeSarifLogsAsync()
if (!_toolKeyToVisitor.TryGetValue(toolKey, out RunMergingVisitor visitor))
{
visitor = _toolKeyToVisitor[toolKey] = new RunMergingVisitor();
_toolKeyToResults[toolKey] = new HashSet<Result>(Result.ValueComparer);
visitor.DeduplicateResults = true;
_toolKeyOrder.Add(toolKey);

// The first run of a given tool + version supplies the merged run's
Expand All @@ -178,17 +176,8 @@ private async Task<bool> MergeSarifLogsAsync()
continue;
}

HashSet<Result> seenResults = _toolKeyToResults[toolKey];
foreach (Result result in run.Results)
{
// Drop results that are value-identical to one already merged for this
// tool. A sharded scan can re-report the same finding in more than one
// input log; the merged run should carry each finding exactly once.
if (!seenResults.Add(result))
{
continue;
}

visitor.CurrentRun = run;
visitor.VisitResult(result.DeepClone());
}
Expand Down
8 changes: 7 additions & 1 deletion src/Sarif/Visitors/RunMergingVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ public class RunMergingVisitor : SarifRewritingVisitor
private List<LogicalLocation> LogicalLocations { get; }
private List<ReportingDescriptor> Rules { get; }
private List<Invocation> Invocations { get; }
private HashSet<Result> SeenResults { get; }

private Dictionary<string, int> RuleIdToIndex { get; }
private Dictionary<OrderSensitiveValueComparisonList<LogicalLocation>, int> LogicalLocationToIndex { get; }
private Dictionary<OrderSensitiveValueComparisonList<Artifact>, int> ArtifactToIndex { get; }
private Dictionary<Run, int> InvocationBaseIndexByRun { get; }

public Run CurrentRun { get; set; }
public bool DeduplicateResults { get; set; }

public RunMergingVisitor()
{
Expand All @@ -46,6 +48,7 @@ public RunMergingVisitor()
LogicalLocations = new List<LogicalLocation>();
Rules = new List<ReportingDescriptor>();
Invocations = new List<Invocation>();
SeenResults = new HashSet<Result>(Result.ValueComparer);

RuleIdToIndex = new Dictionary<string, int>();
LogicalLocationToIndex = new Dictionary<OrderSensitiveValueComparisonList<LogicalLocation>, int>();
Expand Down Expand Up @@ -117,7 +120,10 @@ public override Result VisitResult(Result node)
RemapInvocationIndex(node);

Result result = base.VisitResult(node);
Results.Add(result);
if (!DeduplicateResults || SeenResults.Add(result))
{
Results.Add(result);
}
return result;
}

Expand Down
100 changes: 100 additions & 0 deletions src/Test.UnitTests.Sarif.Multitool.Library/MergeCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

using System;
using System.IO;
using System.Linq;

using FluentAssertions;

using Moq;
using Newtonsoft.Json.Linq;

using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -46,6 +48,55 @@ public void MergeCommand_WhenThereAreDuplicatedResults_ProducesNonDuplicatedResu
RunTest("DuplicatedResults.sarif");
}

[Fact]
public void MergeCommand_WhenDuplicateResultsUseDifferentLocalIndices_ProducesOneResult()
{
string inputFolderPath = Directory.GetCurrentDirectory();
string outputFileName = Guid.NewGuid().ToString() + SarifConstants.SarifFileExtension;
string outputFilePath = Path.Combine(TestOutputDirectory, outputFileName);
var mockFileSystem = new Mock<IFileSystem>();

mockFileSystem.Setup(x => x.FileExists(outputFilePath)).Returns(false);
mockFileSystem.Setup(x => x.DirectoryExists(inputFolderPath)).Returns(true);
mockFileSystem
.Setup(x => x.DirectoryEnumerateFiles(inputFolderPath, "*.sarif", SearchOption.TopDirectoryOnly))
.Returns(new[] { "first_run.sarif", "second_run.sarif" });
mockFileSystem
.Setup(x => x.FileReadAllText("first_run.sarif"))
.Returns(CreateSarifWithLocalIndices(
new[] { "rule-a", "rule-b" },
new[] { "source/a.cpp", "source/b.cpp" },
ruleIndex: 0,
artifactIndex: 0));
mockFileSystem
.Setup(x => x.FileReadAllText("second_run.sarif"))
.Returns(CreateSarifWithLocalIndices(
new[] { "rule-b", "rule-a" },
new[] { "source/b.cpp", "source/a.cpp" },
ruleIndex: 1,
artifactIndex: 1));
mockFileSystem
.Setup(x => x.DirectoryCreateDirectory(TestOutputDirectory))
.Returns((string path) => Directory.CreateDirectory(path));
mockFileSystem
.Setup(x => x.FileCreate(outputFilePath))
.Returns((string path) => File.Create(path));

var options = new MergeOptions
{
OutputDirectoryPath = TestOutputDirectory,
TargetFileSpecifiers = new[] { "*.sarif" },
OutputFileName = outputFileName,
OutputFileOptions = new[] { FilePersistenceOptions.ForceOverwrite, FilePersistenceOptions.PrettyPrint },
};

int returnCode = new MergeCommand(mockFileSystem.Object).Run(options);
returnCode.Should().Be(0);

JToken results = JObject.Parse(File.ReadAllText(outputFilePath))["runs"][0]["results"];
results.Count().Should().Be(1);
}

[Fact]
public void MergeCommand_WhenPassNoFolderOnlyFile_ProducesCorrectResults()
{
Expand Down Expand Up @@ -115,5 +166,54 @@ private void PrepareFileSystemMock(string inputResourceName, string inputFolderP
return;
}
}

private static string CreateSarifWithLocalIndices(
string[] ruleIds,
string[] artifactUris,
int ruleIndex,
int artifactIndex)
{
return JObject.FromObject(new
{
version = "2.1.0",
runs = new[]
{
new
{
tool = new
{
driver = new
{
name = "CodeQL",
version = "1.0",
rules = ruleIds.Select(id => new { id }).ToArray(),
},
},
artifacts = artifactUris
.Select(uri => new { location = new { uri } })
.ToArray(),
results = new[]
{
new
{
ruleIndex,
message = new { text = "same finding" },
locations = new[]
{
new
{
physicalLocation = new
{
artifactLocation = new { index = artifactIndex },
region = new { startLine = 10, startColumn = 1 },
},
},
},
},
},
},
},
}).ToString();
}
}
}