From b87f88e704b04b992a98a634a64306e611b392b2 Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Tue, 8 Nov 2022 13:26:44 -0600 Subject: [PATCH] Feature: Suppression Expression Support (#2530) * Feature: Added GUID list and query suppression support * Fixed bug: suppress options with guids type * fixed bug in SuppressOptions with guids options * Bug: Allowed using results-guids and expression arguments together. * chore: typo in log message * feat: console log number of suppressions applied * bug: comma delimit results arg * Fixed bug with ResultsGuids and added debug output * fixed bug with ResultsGuids union to Query guids * WIP - Added IsSuppressed evaluator and updated unit tests * Fixed: Changes requested for #2530 * bug: validate result guid values are not whitespace * bug: empty search suppresses all results * bug: skip duplicate suppression entries * bug: suppress visitor multiple test cases * bug: fixed merge command unit test failure * chore: formatting failures * chore: codeql warnings * chore: docs * bug: suppression expiration no expiry logic flaw * docs: release notes + query mode updates * bug: fixed failing suppression test case * feat: suppress expiryUtc command argument * bug: reverted bad test data Co-authored-by: Andrew Guggenberger Co-authored-by: Andrew Guggenberger --- src/ReleaseHistory.md | 2 + .../SuppressCommand.cs | 81 ++- .../SuppressOptions.cs | 23 +- src/Sarif/Core/Result.cs | 11 +- src/Sarif/Query/Evaluators/SarifEvaluators.cs | 2 + src/Sarif/Visitors/SuppressVisitor.cs | 46 +- .../MergeCommandUnitTests.cs | 3 +- .../QueryCommandTests.cs | 4 + .../SuppressCommandTests.cs | 234 ++++++--- .../TestData/QueryCommand/elfie-arriba.sarif | 15 +- .../Baseline/DefaultBaselineUnitTests.cs | 2 +- .../Baseline/StrictBaselineUnitTests.cs | 2 +- src/Test.UnitTests.Sarif/Core/ResultTests.cs | 11 +- .../Visitors/SuppressVisitorTests.cs | 476 +++++++++++++++--- .../RandomSarifLogGenerator.cs | 7 +- 15 files changed, 769 insertions(+), 150 deletions(-) diff --git a/src/ReleaseHistory.md b/src/ReleaseHistory.md index 306308cb6..eb2f21dcd 100644 --- a/src/ReleaseHistory.md +++ b/src/ReleaseHistory.md @@ -11,6 +11,8 @@ * BUGFIX: Update `merge` command to properly produce runs by tool and version when passed the `--merge-runs` argument. [#2488](https://github.com/microsoft/sarif-sdk/pull/2488) * BUGFIX: Eliminate `IOException` and `DirectoryNotFoundException` exceptions thrown by `merge` command when splitting by rule (due to invalid file characters in rule ids). [#2513](https://github.com/microsoft/sarif-sdk/pull/2513) * BUGFIX: Fix classes inside NotYetAutoGenerated folder missing `virtual` keyword for public methods and properties, by regenerate and manually sync the changes. [#2537](https://github.com/microsoft/sarif-sdk/pull/2537) +* FEATURE: Enhancement to the `suppress` command to better support auditing results. New argument `--expression` provides the capability to suppress all results matching the expression. New argument `--results-guids` provides the capability to suppress one to many results by the `guid` value. With this update, previously suppressed (non-expired) results will not be suppressed again. [#2530](https://github.com/microsoft/sarif-sdk/pull/2530) +* FEATURE: Enhancement to the `query` command adding a new `IsSuppressed` expression option. This query expression allows auditors to filter results based on their suppression status. The expression finds all suppressed (non-expired) results. [#2530](https://github.com/microsoft/sarif-sdk/pull/2530) ## **v3.1.0** [Sdk](https://www.nuget.org/packages/Sarif.Sdk/3.1.0) | [Driver](https://www.nuget.org/packages/Sarif.Driver/3.1.0) | [Converters](https://www.nuget.org/packages/Sarif.Converters/3.1.0) | [Multitool](https://www.nuget.org/packages/Sarif.Multitool/3.1.0) | [Multitool Library](https://www.nuget.org/packages/Sarif.Multitool.Library/3.1.0) diff --git a/src/Sarif.Multitool.Library/SuppressCommand.cs b/src/Sarif.Multitool.Library/SuppressCommand.cs index 5cf1813fa..2b2590191 100644 --- a/src/Sarif.Multitool.Library/SuppressCommand.cs +++ b/src/Sarif.Multitool.Library/SuppressCommand.cs @@ -2,9 +2,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; + +using Kusto.Cloud.Platform.Utils; using Microsoft.CodeAnalysis.Sarif.Driver; +using Microsoft.CodeAnalysis.Sarif.Query; +using Microsoft.CodeAnalysis.Sarif.Query.Evaluators; using Microsoft.CodeAnalysis.Sarif.Readers; using Microsoft.CodeAnalysis.Sarif.Visitors; using Microsoft.CodeAnalysis.Sarif.Writers; @@ -34,12 +41,41 @@ public int Run(SuppressOptions options) options.Formatting, out string _); + if (!string.IsNullOrWhiteSpace(options.Expression)) + { + var expressionGuids = ReturnQueryExpressionGuids(options); + if (options.ResultsGuids != null && options.ResultsGuids.Where(i => !string.IsNullOrWhiteSpace(i)).Count() > 0) + { + options.ResultsGuids = expressionGuids.Union(options.ResultsGuids.Where(i => !string.IsNullOrWhiteSpace(i))); + } + else + { + options.ResultsGuids = expressionGuids; + } + } + if (options.ResultsGuids != null) + { + Console.WriteLine($"Suppressing {options.ResultsGuids.Count()} of {currentSarifLog.Runs.Sum(i => i.Results.Count)} results."); +#if DEBUG + foreach (var result in options.ResultsGuids) + { + Console.WriteLine($"{result}"); + } +#endif + } + else + { + Console.WriteLine($"Suppressing {currentSarifLog.Runs.Sum(i => i.Results.Count)} of {currentSarifLog.Runs.Sum(i => i.Results.Count)} results."); + } + SarifLog reformattedLog = new SuppressVisitor(options.Justification, options.Alias, options.Guids, options.Timestamps, options.ExpiryInDays, - options.Status).VisitSarifLog(currentSarifLog); + options.ExpiryUtc, + options.Status, + options.ResultsGuids).VisitSarifLog(currentSarifLog); string actualOutputPath = CommandUtilities.GetTransformedOutputFileName(options); if (options.SarifOutputVersion == SarifVersion.OneZeroZero) @@ -55,7 +91,7 @@ public int Run(SuppressOptions options) } w.Stop(); - Console.WriteLine($"Supress completed in {w.Elapsed}."); + Console.WriteLine($"Suppress completed in {w.Elapsed}."); } catch (Exception ex) { @@ -66,12 +102,53 @@ public int Run(SuppressOptions options) return SUCCESS; } + private IEnumerable ReturnQueryExpressionGuids(SuppressOptions options) + { + int originalTotal = 0; + int matchCount = 0; + // Parse the Query and create a Result evaluator for it + IExpression expression = ExpressionParser.ParseExpression(options.Expression); + IExpressionEvaluator evaluator = expression.ToEvaluator(SarifEvaluators.ResultEvaluator); + + // Read the log + SarifLog log = ReadSarifFile(this.FileSystem, options.InputFilePath); + + foreach (Run run in log.Runs) + { + if (run.Results == null) { continue; } + run.SetRunOnResults(); + + originalTotal += run.Results.Count; + + // Find matches for Results in the Run + BitArray matches = new BitArray(run.Results.Count); + evaluator.Evaluate(run.Results, matches); + + // Count the new matches + matchCount += matches.TrueCount(); + + // Filter the Run.Results to the matches + run.Results = matches.MatchingSubset(run.Results); + } + + // Remove any Runs with no remaining matches + log.Runs = log.Runs.Where(r => (r?.Results?.Count ?? 0) > 0).ToList(); + var guids = log.Runs.SelectMany(x => x.Results.Select(y => y.Guid)).ToList(); + + return guids; + } + private bool ValidateOptions(SuppressOptions options) { bool valid = true; valid &= options.Validate(); valid &= options.ExpiryInDays >= 0; + if (options.ExpiryUtc.HasValue) + { + valid &= options.ExpiryUtc.Value > DateTime.UtcNow; + valid &= options.ExpiryInDays == 0; + } valid &= !string.IsNullOrWhiteSpace(options.Justification); valid &= (options.Status == SuppressionStatus.Accepted || options.Status == SuppressionStatus.UnderReview); valid &= DriverUtilities.ReportWhetherOutputFileCanBeCreated(options.OutputFilePath, options.Force, FileSystem); diff --git a/src/Sarif.Multitool.Library/SuppressOptions.cs b/src/Sarif.Multitool.Library/SuppressOptions.cs index ef2162763..e6a5de957 100644 --- a/src/Sarif.Multitool.Library/SuppressOptions.cs +++ b/src/Sarif.Multitool.Library/SuppressOptions.cs @@ -1,6 +1,9 @@ // 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.Collections.Generic; + using CommandLine; using Microsoft.CodeAnalysis.Sarif.Driver; @@ -26,6 +29,19 @@ public class SuppressOptions : SingleFileOptionsBase HelpText = "A UUID that will be associated with a suppression.")] public bool Guids { get; set; } + [Option( + "results-guids", + HelpText = "A comma delimited list of SARIF log result guid(s) to suppress.", + Default = null, + Separator = ',')] + public IEnumerable ResultsGuids { get; set; } + + [Option( + 'e', + "expression", + HelpText = "Result Expression to Evaluate (ex: (BaselineState != 'Unchanged'))")] + public string Expression { get; set; } + [Option( "timestamps", HelpText = "The property 'timeUtc' that will be associated with a suppression.")] @@ -33,9 +49,14 @@ public class SuppressOptions : SingleFileOptionsBase [Option( "expiryInDays", - HelpText = "The property 'expiryUtc' that will be associated with a suppression from the 'timeUtc'.")] + HelpText = "The property 'expiryUtc' that will be associated with a suppression from the 'timeUtc'. Cannot be used with 'expiryUtc'.")] public int ExpiryInDays { get; set; } + [Option( + "expiryUtc", + HelpText = "The property 'expiryUtc' that will be associated with a suppression. Cannot be used with 'expiryInDays'.")] + public DateTime? ExpiryUtc { get; set; } + [Option( "status", HelpText = "The status that will be used in the suppression. Valid values include Accepted and UnderReview.")] diff --git a/src/Sarif/Core/Result.cs b/src/Sarif/Core/Result.cs index 88c7f4882..57d33f717 100644 --- a/src/Sarif/Core/Result.cs +++ b/src/Sarif/Core/Result.cs @@ -99,7 +99,7 @@ public ReportingDescriptor GetRule(Run run = null) return new ReportingDescriptor() { Id = this.RuleId ?? this.Rule?.Id }; } - public bool TryIsSuppressed(out bool isSuppressed) + public bool TryIsSuppressed(out bool isSuppressed, bool checkExpired = false) { isSuppressed = false; if (this == null) @@ -120,8 +120,15 @@ public bool TryIsSuppressed(out bool isSuppressed) // If the status of any of the suppressions is "underReview" or "rejected", // then the result should not be considered suppressed. Otherwise, the result should be considered suppressed. - // https://github.com/microsoft/sarif-tutorials/blob/main/docs/Displaying-results-in-a-viewer.md#determining-suppression-status + // https://github.com/microsoft/sarif-tutorials/blob/main/docs/Displaying-results-in-a-viewer.md#determining-suppression-status isSuppressed = !suppressions.Any(s => s.Status == SuppressionStatus.UnderReview || s.Status == SuppressionStatus.Rejected); + + // if we have suppressions, check expiration + if (isSuppressed && checkExpired) + { + isSuppressed = suppressions.Any(s => (!s.TryGetProperty("expiryUtc", out DateTime noExpiryUtc) || (s.TryGetProperty("expiryUtc", out DateTime expiryUtc) && expiryUtc > DateTime.UtcNow)) && s.Status == SuppressionStatus.Accepted); + } + return true; } diff --git a/src/Sarif/Query/Evaluators/SarifEvaluators.cs b/src/Sarif/Query/Evaluators/SarifEvaluators.cs index fc27cacac..609a70067 100644 --- a/src/Sarif/Query/Evaluators/SarifEvaluators.cs +++ b/src/Sarif/Query/Evaluators/SarifEvaluators.cs @@ -34,6 +34,8 @@ public static IExpressionEvaluator ResultEvaluator(TermExpression term) return new DoubleEvaluator(r => r.Rank, term); case "ruleid": return new StringEvaluator(r => r.GetRule(r.Run).Id, term, StringComparison.OrdinalIgnoreCase); + case "issuppressed": + return new BoolEvaluator(r => r.TryIsSuppressed(out bool suppressed) && suppressed, term); case "uri": // Ensure the Run is provided, to look up Uri from Run.Artifacts when needed. diff --git a/src/Sarif/Visitors/SuppressVisitor.cs b/src/Sarif/Visitors/SuppressVisitor.cs index 91617b019..a77855f05 100644 --- a/src/Sarif/Visitors/SuppressVisitor.cs +++ b/src/Sarif/Visitors/SuppressVisitor.cs @@ -3,35 +3,40 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.CodeAnalysis.Sarif.Visitors { public class SuppressVisitor : SarifRewritingVisitor { - private readonly bool guids; + private readonly bool uuids; + private readonly IEnumerable resultsGuids; private readonly string alias; private readonly bool timestamps; private readonly DateTime timeUtc; - private readonly DateTime expiryUtc; private readonly int expiryInDays; + private readonly DateTime? expiryUtc; private readonly string justification; private readonly SuppressionStatus suppressionStatus; public SuppressVisitor(string justification, string alias, - bool guids, + bool uuids, bool timestamps, int expiryInDays, - SuppressionStatus suppressionStatus) + DateTime? expiryUtc, + SuppressionStatus suppressionStatus, + IEnumerable resultsGuids) { this.alias = alias; - this.guids = guids; + this.uuids = uuids; this.timestamps = timestamps; this.timeUtc = DateTime.UtcNow; this.expiryInDays = expiryInDays; + this.expiryUtc = expiryUtc; this.justification = justification; this.suppressionStatus = suppressionStatus; - this.expiryUtc = this.timeUtc.AddDays(expiryInDays); + this.resultsGuids = resultsGuids; } public override Result VisitResult(Result node) @@ -41,6 +46,13 @@ public override Result VisitResult(Result node) node.Suppressions = new List(); } + // Skip if node is already suppressed + bool isSuppressed = false; + if (node.TryIsSuppressed(out isSuppressed, true) && isSuppressed) + { + return base.VisitResult(node); + } + var suppression = new Suppression { Status = suppressionStatus, @@ -53,7 +65,7 @@ public override Result VisitResult(Result node) suppression.SetProperty(nameof(alias), alias); } - if (guids) + if (this.uuids) { suppression.Guid = Guid.NewGuid(); } @@ -65,10 +77,26 @@ public override Result VisitResult(Result node) if (expiryInDays > 0) { - suppression.SetProperty(nameof(expiryUtc), expiryUtc); + suppression.SetProperty(nameof(expiryUtc), timeUtc.AddDays(expiryInDays)); + } + + if (expiryUtc.HasValue) + { + suppression.SetProperty(nameof(expiryUtc), expiryUtc.Value); + } + + if (this.resultsGuids != null) + { + if (this.resultsGuids.Contains(node.Guid, StringComparer.OrdinalIgnoreCase)) + { + node.Suppressions.Add(suppression); + } + } + else + { + node.Suppressions.Add(suppression); } - node.Suppressions.Add(suppression); return base.VisitResult(node); } } diff --git a/src/Test.UnitTests.Sarif.Multitool.Library/MergeCommandUnitTests.cs b/src/Test.UnitTests.Sarif.Multitool.Library/MergeCommandUnitTests.cs index 607405459..61b118d2a 100644 --- a/src/Test.UnitTests.Sarif.Multitool.Library/MergeCommandUnitTests.cs +++ b/src/Test.UnitTests.Sarif.Multitool.Library/MergeCommandUnitTests.cs @@ -291,12 +291,13 @@ private Run CreateTestRun(int numberOfResult, bool createSubRule = false, string run.Results ??= new List(); var artifactUri = new Uri("path/to/file", UriKind.Relative); + var guid = Guid.NewGuid().ToString(); for (int i = 1; i <= numberOfResult; i++) { string ruleId = createSubRule ? $"TESTRULE/00{i}" : $"TESTRULE00{i}"; run.Results.AddRange( - RandomSarifLogGenerator.GenerateFakeResults(this.random, new List { ruleId }, new List { artifactUri }, 1)); + RandomSarifLogGenerator.GenerateFakeResults(this.random, new List { ruleId }, new List() { guid }, new List { artifactUri }, 1)); } return run; diff --git a/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs b/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs index fd2d6e0c5..91c6329a2 100644 --- a/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs +++ b/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs @@ -30,6 +30,10 @@ public void QueryCommand_Basics() RunAndVerifyCount(1, new QueryOptions() { Expression = "Level != Error", InputFilePath = filePath }); RunAndVerifyCount(1, new QueryOptions() { Expression = "Level != Error && RuleId = CSCAN0060/0", InputFilePath = filePath }); + // Suppression filtering + RunAndVerifyCount(1, new QueryOptions() { Expression = "IsSuppressed == True", InputFilePath = filePath }); + RunAndVerifyCount(1, new QueryOptions() { Expression = "IsSuppressed == True && RuleId = CSCAN0060/0", InputFilePath = filePath }); + // Intersection w/no matches RunAndVerifyCount(0, new QueryOptions() { Expression = "Level != Error && RuleId != CSCAN0060/0", InputFilePath = filePath }); diff --git a/src/Test.UnitTests.Sarif.Multitool.Library/SuppressCommandTests.cs b/src/Test.UnitTests.Sarif.Multitool.Library/SuppressCommandTests.cs index 3ec492c65..772565107 100644 --- a/src/Test.UnitTests.Sarif.Multitool.Library/SuppressCommandTests.cs +++ b/src/Test.UnitTests.Sarif.Multitool.Library/SuppressCommandTests.cs @@ -1,8 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// 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.Collections.Generic; +using System.IO; using System.Text; using FluentAssertions; @@ -56,6 +57,29 @@ public void SuppressCommand_ShouldReturnFailure_WhenBadArgumentsAreSupplied() OutputFilePath = outputPath, Status = SuppressionStatus.Accepted }, + new SuppressOptions + { + ExpiryInDays = 1, + Justification = "some justification", + OutputFilePath = outputPath, + Expression = "fail", + Status = SuppressionStatus.Accepted + }, + new SuppressOptions + { + ExpiryUtc = DateTime.UtcNow.AddDays(-1), + Justification = "some justification", + OutputFilePath = outputPath, + Status = SuppressionStatus.Accepted + }, + new SuppressOptions + { + ExpiryInDays = 1, + ExpiryUtc = DateTime.UtcNow.AddDays(1), + Justification = "some justification", + OutputFilePath = outputPath, + Status = SuppressionStatus.Accepted + }, }; var mock = new Mock(); @@ -74,37 +98,108 @@ public void SuppressCommand_ShouldReturnSuccess_WhenCorrectArgumentsAreSupplied( { var optionsTestCases = new SuppressOptions[] { - new SuppressOptions - { - Alias = "some alias", - InputFilePath = @"C:\input.sarif", - OutputFilePath = @"C:\output.sarif", - Justification = "some justification", - Status = SuppressionStatus.Accepted - }, - new SuppressOptions - { - InputFilePath = @"C:\input.sarif", - OutputFilePath = @"C:\output.sarif", - Justification = "some justification", - Status = SuppressionStatus.UnderReview - }, + // new SuppressOptions + // { + // Alias = "some alias", + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // Status = SuppressionStatus.Accepted + // }, + // new SuppressOptions + // { + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // Status = SuppressionStatus.UnderReview + // }, + // new SuppressOptions + // { + // Guids = true, + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // Status = SuppressionStatus.Accepted + // }, + // new SuppressOptions + // { + // Guids = true, + // ExpiryInDays = 5, + // Timestamps = true, + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // Status = SuppressionStatus.Accepted + // }, + // new SuppressOptions + // { + // Guids = true, + // ExpiryInDays = 5, + // Timestamps = true, + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // Expression = "BaseLineState = \"New\"", + // Status = SuppressionStatus.Accepted + // }, + // new SuppressOptions + // { + // Guids = true, + // ExpiryInDays = 5, + // Timestamps = true, + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // ResultsGuids = new List() { "GUID"}, + // Expression = string.Empty, + // Status = SuppressionStatus.Accepted + // }, + // new SuppressOptions + // { + // Guids = true, + // ExpiryInDays = 5, + // Timestamps = true, + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // ResultsGuids = new List() { "GUID", "GUID2"}, + // Expression = "BaseLineState = \"New\"", + // Status = SuppressionStatus.Accepted + // }, + // new SuppressOptions + // { + // Guids = true, + // ExpiryInDays = 5, + // Timestamps = true, + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // ResultsGuids = new List() {}, + // Expression = "BaseLineState = \"New\"", + // Status = SuppressionStatus.Accepted + // }, + // new SuppressOptions + // { + // Guids = true, + // ExpiryInDays = 5, + // Timestamps = true, + // InputFilePath = @"C:\input.sarif", + // OutputFilePath = @"C:\output.sarif", + // Justification = "some justification", + // ResultsGuids = new List() {}, + // Expression = "IsSuppressed == False", + // Status = SuppressionStatus.Accepted + // }, new SuppressOptions { Guids = true, - InputFilePath = @"C:\input.sarif", - OutputFilePath = @"C:\output.sarif", - Justification = "some justification", - Status = SuppressionStatus.Accepted - }, - new SuppressOptions - { - Guids = true, - ExpiryInDays = 5, + ExpiryUtc = DateTime.UtcNow.AddDays(30), Timestamps = true, InputFilePath = @"C:\input.sarif", OutputFilePath = @"C:\output.sarif", Justification = "some justification", + ResultsGuids = new List() {}, + Expression = "IsSuppressed == False", Status = SuppressionStatus.Accepted }, }; @@ -127,53 +222,76 @@ private static void VerifySuppressCommand(SuppressOptions options) { new Result { - RuleId = "Test0001" + RuleId = "Test0001", + Guid = "GUID", + BaselineState = BaselineState.New } } } } }; - var transformedContents = new StringBuilder(); - var mockFileSystem = new Mock(); - mockFileSystem - .Setup(x => x.FileReadAllText(options.InputFilePath)) - .Returns(JsonConvert.SerializeObject(current)); + using (MemoryStream currentStream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(current)))) + { + var transformedContents = new StringBuilder(); + var mockFileSystem = new Mock(); - mockFileSystem - .Setup(x => x.FileCreate(options.OutputFilePath)) - .Returns(() => new MemoryStreamToStringBuilder(transformedContents)); + mockFileSystem + .Setup(x => x.FileReadAllText(options.InputFilePath)) + .Returns(JsonConvert.SerializeObject(current)); - var command = new SuppressCommand(mockFileSystem.Object); - command.Run(options).Should().Be(CommandBase.SUCCESS); + mockFileSystem + .Setup(x => x.FileOpenRead(options.InputFilePath)) + .Returns(() => + currentStream); - SarifLog suppressed = JsonConvert.DeserializeObject(transformedContents.ToString()); - suppressed.Runs[0].Results[0].Suppressions.Should().NotBeNullOrEmpty(); + mockFileSystem + .Setup(x => x.FileCreate(options.OutputFilePath)) + .Returns(() => new MemoryStreamToStringBuilder(transformedContents)); - Suppression suppression = suppressed.Runs[0].Results[0].Suppressions[0]; - suppression.Status.Should().Be(options.Status); - suppression.Kind.Should().Be(SuppressionKind.External); - suppression.Justification.Should().Be(options.Justification); + var command = new SuppressCommand(mockFileSystem.Object); + command.Run(options).Should().Be(CommandBase.SUCCESS); - if (!string.IsNullOrWhiteSpace(options.Alias)) - { - suppression.GetProperty("alias").Should().Be(options.Alias); - } + SarifLog suppressed = JsonConvert.DeserializeObject(transformedContents.ToString()); + suppressed.Runs[0].Results[0].Suppressions.Should().NotBeNullOrEmpty(); - if (options.Guids) - { - suppression.Guid.Should().NotBeNull(); - } + Suppression suppression = suppressed.Runs[0].Results[0].Suppressions[0]; + suppression.Status.Should().Be(options.Status); + suppression.Kind.Should().Be(SuppressionKind.External); + suppression.Justification.Should().Be(options.Justification); - if (options.Timestamps && suppression.TryGetProperty("timeUtc", out DateTime timeUtc)) - { - timeUtc.Should().BeCloseTo(DateTime.UtcNow, DateTimeAssertPrecision); - } + if (!string.IsNullOrWhiteSpace(options.Alias)) + { + suppression.GetProperty("alias").Should().Be(options.Alias); + } - if (options.ExpiryInDays > 0 && suppression.TryGetProperty("expiryUtc", out DateTime expiryUtc)) - { - expiryUtc.Should().BeCloseTo(DateTime.UtcNow.AddDays(options.ExpiryInDays), DateTimeAssertPrecision); + if (options.Guids) + { + suppression.Guid.Should().NotBeNullOrEmpty(); + } + + if (!string.IsNullOrWhiteSpace(options.Expression)) + { + suppressed.Runs[0].Results[0].BaselineState.Should().Be(BaselineState.New); + } + + if (options.Timestamps && suppression.TryGetProperty("timeUtc", out DateTime timeUtc)) + { + timeUtc.Should().BeCloseTo(DateTime.UtcNow, DateTimeAssertPrecision); + } + + if (options.ExpiryInDays > 0) + { + suppression.TryGetProperty("expiryUtc", out DateTime expiryInDaysUtc).Should().BeTrue(); + expiryInDaysUtc.Should().BeCloseTo(DateTime.UtcNow.AddDays(options.ExpiryInDays), DateTimeAssertPrecision); + } + + if (options.ExpiryUtc.HasValue) + { + suppression.TryGetProperty("expiryUtc", out DateTime expiryUtc).Should().BeTrue(); + expiryUtc.Should().BeCloseTo(options.ExpiryUtc.Value, DateTimeAssertPrecision); + } } } } -} +} \ No newline at end of file diff --git a/src/Test.UnitTests.Sarif.Multitool.Library/TestData/QueryCommand/elfie-arriba.sarif b/src/Test.UnitTests.Sarif.Multitool.Library/TestData/QueryCommand/elfie-arriba.sarif index f56f82c05..46f52fe82 100644 --- a/src/Test.UnitTests.Sarif.Multitool.Library/TestData/QueryCommand/elfie-arriba.sarif +++ b/src/Test.UnitTests.Sarif.Multitool.Library/TestData/QueryCommand/elfie-arriba.sarif @@ -74,7 +74,18 @@ }, "properties": { "matchState": "NotSuppressed" - } + }, + "suppressions": [ + { + "kind": "external", + "status": "accepted", + "justification": "justification", + "properties": { + "alias": "alias", + "expiryUtc": "2022-09-08T19:22:23.438465Z" + } + } + ] }, { "ruleId": "CSCAN0060/0", @@ -198,7 +209,7 @@ }, "properties": { "matchState": "NotSuppressed" - } + } } ], "automationDetails": {}, diff --git a/src/Test.UnitTests.Sarif/Baseline/DefaultBaselineUnitTests.cs b/src/Test.UnitTests.Sarif/Baseline/DefaultBaselineUnitTests.cs index d3e491782..9498f1a88 100644 --- a/src/Test.UnitTests.Sarif/Baseline/DefaultBaselineUnitTests.cs +++ b/src/Test.UnitTests.Sarif/Baseline/DefaultBaselineUnitTests.cs @@ -42,7 +42,7 @@ public void DefaultBaseline_NewResultAdded_New() Random random = RandomSarifLogGenerator.GenerateRandomAndLog(this.output); Run baseline = RandomSarifLogGenerator.GenerateRandomRunWithoutDuplicateIssues(random, DefaultBaseline.ResultBaselineEquals.DefaultInstance, random.Next(100) + 5); Run next = baseline.DeepClone(); - next.Results.Add(RandomSarifLogGenerator.GenerateFakeResults(random, new List() { "NEWTESTRESULT" }, new List() { new Uri(@"c:\test\testfile") }, 1).First()); + next.Results.Add(RandomSarifLogGenerator.GenerateFakeResults(random, new List() { "NEWTESTRESULT" }, new List() { "NEWGUID" }, new List() { new Uri(@"c:\test\testfile") }, 1).First()); Run result = defaultBaseliner.CreateBaselinedRun(baseline, next); diff --git a/src/Test.UnitTests.Sarif/Baseline/StrictBaselineUnitTests.cs b/src/Test.UnitTests.Sarif/Baseline/StrictBaselineUnitTests.cs index fa778d22e..fb58cfff8 100644 --- a/src/Test.UnitTests.Sarif/Baseline/StrictBaselineUnitTests.cs +++ b/src/Test.UnitTests.Sarif/Baseline/StrictBaselineUnitTests.cs @@ -42,7 +42,7 @@ public void StrictBaseline_NewResultAdded_New() Random random = RandomSarifLogGenerator.GenerateRandomAndLog(this.output); Run baseline = RandomSarifLogGenerator.GenerateRandomRunWithoutDuplicateIssues(random, Result.ValueComparer, random.Next(100) + 5); Run next = baseline.DeepClone(); - next.Results.Add(RandomSarifLogGenerator.GenerateFakeResults(random, new List() { "NEWTESTRESULT" }, new List() { new Uri(@"c:\test\testfile") }, 1).First()); + next.Results.Add(RandomSarifLogGenerator.GenerateFakeResults(random, new List() { "NEWTESTRESULT" }, new List() { "NEWGUID" }, new List() { new Uri(@"c:\test\testfile") }, 1).First()); Run result = strictBaseliner.CreateBaselinedRun(baseline, next); diff --git a/src/Test.UnitTests.Sarif/Core/ResultTests.cs b/src/Test.UnitTests.Sarif/Core/ResultTests.cs index 094e84e06..614dfd82c 100644 --- a/src/Test.UnitTests.Sarif/Core/ResultTests.cs +++ b/src/Test.UnitTests.Sarif/Core/ResultTests.cs @@ -1,6 +1,7 @@ // 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.Collections.Generic; using FluentAssertions; @@ -55,8 +56,9 @@ public void Result_TryIsSuppressed_ShouldReturnTrueWhenSuppressionsAreAvailable( { var result = new Result { - Suppressions = new List() + Suppressions = new List() { } }; + result.TryIsSuppressed(out bool isSuppressed).Should().BeTrue(); isSuppressed.Should().BeFalse(); @@ -86,6 +88,13 @@ public void Result_TryIsSuppressed_ShouldReturnTrueWhenSuppressionsAreAvailable( result.Suppressions.Add(new Suppression { Status = SuppressionStatus.Accepted }); result.TryIsSuppressed(out isSuppressed).Should().BeTrue(); isSuppressed.Should().BeTrue(); + + // Suppression with 'Accepted' only and expired. + result.Suppressions.Clear(); + result.Suppressions.Add(new Suppression { Status = SuppressionStatus.Accepted }); + result.Suppressions[0].SetProperty("expiryUtc", DateTime.UtcNow.AddDays(1)); + result.TryIsSuppressed(out isSuppressed, true).Should().BeTrue(); + isSuppressed.Should().BeTrue(); } } } diff --git a/src/Test.UnitTests.Sarif/Visitors/SuppressVisitorTests.cs b/src/Test.UnitTests.Sarif/Visitors/SuppressVisitorTests.cs index ce63b938b..c7169edeb 100644 --- a/src/Test.UnitTests.Sarif/Visitors/SuppressVisitorTests.cs +++ b/src/Test.UnitTests.Sarif/Visitors/SuppressVisitorTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentAssertions; @@ -17,90 +18,406 @@ public class SuppressVisitorTests private const int DateTimeAssertPrecision = 500; [Fact] - public void SuppressVisitor_ShouldFlowPropertiesCorrectly() + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_EmptyAlias() { - var testCases = new[] + var testCase = new { - new - { - Alias = string.Empty, - Justification = "some suppress justification", - Guids = false, - Timestamps = false, - ExpiryInDays = 0, - SuppressionStatus = SuppressionStatus.Accepted - }, - new - { - Alias = "some alias", - Justification = "some suppress justification", - Guids = false, - Timestamps = false, - ExpiryInDays = 0, - SuppressionStatus = SuppressionStatus.Accepted - }, - new - { - Alias = "some alias", - Justification = "some suppress justification", - Guids = true, - Timestamps = false, - ExpiryInDays = 0, - SuppressionStatus = SuppressionStatus.Accepted - }, - new - { - Alias = "some alias", - Justification = "some suppress justification", - Guids = true, - Timestamps = true, - ExpiryInDays = 0, - SuppressionStatus = SuppressionStatus.Accepted - }, - new - { - Alias = "some alias", - Justification = "some suppress justification", - Guids = true, - Timestamps = true, - ExpiryInDays = 1, - SuppressionStatus = SuppressionStatus.Accepted - }, - new - { - Alias = "some alias", - Justification = "some suppress justification", - Guids = true, - Timestamps = true, - ExpiryInDays = 1, - SuppressionStatus = SuppressionStatus.UnderReview - }, + alias = string.Empty, + justification = "some suppress justification", + uuids = false, + timestamps = false, + expiryInDays = 0, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_With_Alias() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = false, + timestamps = false, + expiryInDays = 0, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_With_Uuids() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = false, + expiryInDays = 0, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_With_Timestamps() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = true, + expiryInDays = 0, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_With_Expiry() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = true, + expiryInDays = 1, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_With_ExpiryDateUtc() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = true, + expiryInDays = 0, + expiryUtc = DateTime.UtcNow.AddDays(30), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), }; - foreach (var testCase in testCases) + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_UnderReview() + { + var testCase = new { - VerifySuppressVisitor(testCase.Alias, - testCase.Justification, - testCase.Guids, - testCase.Timestamps, - testCase.ExpiryInDays, - testCase.SuppressionStatus); + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = true, + expiryInDays = 1, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.UnderReview, + resultGuids = default(List), + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_With_Result_Guids() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = true, + expiryInDays = 1, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = new List() { "704cf481-0cfd-46ae-90cd-533cdc6c3bb4", "ecaa7988-5cef-411b-b468-6c20851d6994", "c65b76c7-3cd6-4381-9216-430bcc7fab2d", "04753e26-d297-43e2-a7f7-ae2d34c398c9", "54cb1f58-f401-4f8e-8f42-f2482a123b85" }, + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldFlowPropertiesCorrectly_Empty_Result_Guids() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = true, + expiryInDays = 1, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = new List() { }, + }; + + VerifySuppressVisitor(testCase.alias, + testCase.justification, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + } + + [Fact] + public void SuppressVisitor_ShouldNotDuplicateEntries_NoExpiry() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = false, + expiryInDays = 0, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + var random = new Random(); + SarifLog current = RandomSarifLogGenerator.GenerateSarifLogWithRuns(random, runCount: 1, resultCount: 1); + + var visitor = new SuppressVisitor(testCase.justification, + testCase.alias, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + + SarifLog suppressed = visitor.VisitSarifLog(current); + IList results = suppressed.Runs[0].Results; + + // Verify suppression was added + foreach (Result result in results) + { + result.Suppressions.Should().NotBeNullOrEmpty(); + result.Suppressions.Count.Should().Be(1); + } + + // Suppress a second time + SarifLog suppressed2 = visitor.VisitSarifLog(suppressed); + IList results2 = suppressed2.Runs[0].Results; + + // Verify duplicate suppression was not added + foreach (Result result in results2) + { + result.Suppressions.Should().NotBeNullOrEmpty(); + result.Suppressions.Count.Should().Be(1); } + + } + + [Fact] + public void SuppressVisitor_ShouldNotDuplicateEntries_ValidExpiry() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = false, + expiryInDays = 3, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + var random = new Random(); + SarifLog current = RandomSarifLogGenerator.GenerateSarifLogWithRuns(random, runCount: 1, resultCount: 1); + + var visitor = new SuppressVisitor(testCase.justification, + testCase.alias, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + + SarifLog suppressed = visitor.VisitSarifLog(current); + IList results = suppressed.Runs[0].Results; + + // Verify suppression was added + foreach (Result result in results) + { + result.Suppressions.Should().NotBeNullOrEmpty(); + result.Suppressions.Count.Should().Be(1); + } + + // Suppress a second time + SarifLog suppressed2 = visitor.VisitSarifLog(suppressed); + IList results2 = suppressed2.Runs[0].Results; + + // Verify duplicate suppression was not added + foreach (Result result in results2) + { + result.Suppressions.Should().NotBeNullOrEmpty(); + result.Suppressions.Count.Should().Be(1); + } + + } + + [Fact] + public void SuppressVisitor_ShouldDuplicateEntries_InvalidExpiry() + { + var testCase = new + { + alias = "some alias", + justification = "some suppress justification", + uuids = true, + timestamps = false, + expiryInDays = 1, + expiryUtc = default(DateTime?), + suppressionStatus = SuppressionStatus.Accepted, + resultGuids = default(List), + }; + + var random = new Random(); + SarifLog current = RandomSarifLogGenerator.GenerateSarifLogWithRuns(random, runCount: 1, resultCount: 1); + + var visitor = new SuppressVisitor(testCase.justification, + testCase.alias, + testCase.uuids, + testCase.timestamps, + testCase.expiryInDays, + testCase.expiryUtc, + testCase.suppressionStatus, + testCase.resultGuids); + + SarifLog suppressed = visitor.VisitSarifLog(current); + IList results = suppressed.Runs[0].Results; + + // Verify suppression was added + foreach (Result result in results) + { + result.Suppressions.Should().NotBeNullOrEmpty(); + result.Suppressions.Count.Should().Be(1); + } + + // Force suppressions to be expired + foreach (Result result in results) + { + foreach (Suppression suppression in result.Suppressions) + { + suppression.SetProperty("expiryUtc", DateTime.UtcNow.AddDays(-1)); + } + } + + // Suppress a second time + SarifLog suppressed2 = visitor.VisitSarifLog(suppressed); + IList results2 = suppressed2.Runs[0].Results; + + // Verify new suppression not added + foreach (Result result in results2) + { + result.Suppressions.Should().NotBeNullOrEmpty(); + result.Suppressions.Count.Should().Be(2); + } + } private static void VerifySuppressVisitor(string alias, string justification, - bool guids, + bool uuids, bool timestamps, int expiryInDays, - SuppressionStatus suppressionStatus) + DateTime? expiryUtc, + SuppressionStatus suppressionStatus, + IEnumerable guids) { var visitor = new SuppressVisitor(justification, alias, - guids, + uuids, timestamps, expiryInDays, - suppressionStatus); + expiryUtc, + suppressionStatus, + guids); var random = new Random(); SarifLog current = RandomSarifLogGenerator.GenerateSarifLogWithRuns(random, runCount: 1, resultCount: 1); @@ -108,6 +425,13 @@ private static void VerifySuppressVisitor(string alias, IList results = suppressed.Runs[0].Results; foreach (Result result in results) { + //Suppressions will not exist if guids is an empty search + if (guids != null && !guids.Any()) + { + result.Suppressions.Should().BeNullOrEmpty(); + return; + } + result.Suppressions.Should().NotBeNullOrEmpty(); Suppression suppression = result.Suppressions[0]; @@ -120,7 +444,7 @@ private static void VerifySuppressVisitor(string alias, suppression.GetProperty("alias").Should().Be(alias); } - if (guids) + if (uuids) { suppression.Guid.Should().NotBeNull(); } @@ -130,9 +454,21 @@ private static void VerifySuppressVisitor(string alias, timeUtc.Should().BeCloseTo(DateTime.UtcNow, DateTimeAssertPrecision); } - if (expiryInDays > 0 && suppression.TryGetProperty("expiryUtc", out DateTime expiryUtc)) + if (expiryInDays > 0) + { + suppression.TryGetProperty("expiryUtc", out DateTime expiryInDaysUtc).Should().BeTrue(); + expiryInDaysUtc.Should().BeCloseTo(DateTime.UtcNow.AddDays(expiryInDays), DateTimeAssertPrecision); + } + + if (expiryUtc.HasValue) + { + suppression.TryGetProperty("expiryUtc", out DateTime expiryTimestampUtc).Should().BeTrue(); + expiryTimestampUtc.Should().BeCloseTo(expiryUtc.Value, DateTimeAssertPrecision); + } + + if (guids != null) { - expiryUtc.Should().BeCloseTo(DateTime.UtcNow.AddDays(expiryInDays), DateTimeAssertPrecision); + suppression.Should().Match(b => (guids.Contains(result.Guid, StringComparer.OrdinalIgnoreCase))); } } } diff --git a/src/Test.Utilities.Sarif/RandomSarifLogGenerator.cs b/src/Test.Utilities.Sarif/RandomSarifLogGenerator.cs index ccaf9e695..25b81a178 100644 --- a/src/Test.Utilities.Sarif/RandomSarifLogGenerator.cs +++ b/src/Test.Utilities.Sarif/RandomSarifLogGenerator.cs @@ -49,6 +49,7 @@ public static SarifLog GenerateSarifLogWithRuns(Random randomGen, int runCount, public static Run GenerateRandomRun(Random random, int? resultCount = null, RandomDataFields dataFields = RandomDataFields.None) { List ruleIds = new List() { "TEST001", "TEST002", "TEST003", "TEST004", "TEST005" }; + List guids = new List() { "704cf481-0cfd-46ae-90cd-533cdc6c3bb4", "ecaa7988-5cef-411b-b468-6c20851d6994", "c65b76c7-3cd6-4381-9216-430bcc7fab2d", "04753e26-d297-43e2-a7f7-ae2d34c398c9", "54cb1f58-f401-4f8e-8f42-f2482a123b85" }; List filePaths = GenerateFakeFiles(GeneratorBaseUri, random.Next(20) + 1).Select(a => new Uri(a)).ToList(); int results = resultCount == null ? random.Next(100) : (int)resultCount; @@ -64,7 +65,7 @@ public static Run GenerateRandomRun(Random random, int? resultCount = null, Rand } }, Artifacts = GenerateFiles(filePaths), - Results = GenerateFakeResults(random, ruleIds, filePaths, results, dataFields) + Results = GenerateFakeResults(random, ruleIds, guids, filePaths, results, dataFields) }; } @@ -96,17 +97,19 @@ public static IEnumerable GenerateFakeFiles(string baseAddress, int coun return results; } - public static IList GenerateFakeResults(Random random, List ruleIds, List filePaths, int resultCount, RandomDataFields dataFields = RandomDataFields.None) + public static IList GenerateFakeResults(Random random, List ruleIds, List guids, List filePaths, int resultCount, RandomDataFields dataFields = RandomDataFields.None) { List results = new List(); for (int i = 0; i < resultCount; i++) { int fileIndex = random.Next(filePaths.Count); int ruleIndex = random.Next(ruleIds.Count); + int guidIndex = random.Next(guids.Count); results.Add(new Result() { RuleId = ruleIds[ruleIndex], RuleIndex = ruleIndex, + Guid = guids[guidIndex], Locations = new Location[] { new Location