Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/ReleaseHistory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
81 changes: 79 additions & 2 deletions src/Sarif.Multitool.Library/SuppressCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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)
{
Expand All @@ -66,12 +102,53 @@ public int Run(SuppressOptions options)
return SUCCESS;
}

private IEnumerable<string> 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<Result> evaluator = expression.ToEvaluator<Result>(SarifEvaluators.ResultEvaluator);

// Read the log
SarifLog log = ReadSarifFile<SarifLog>(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<Result>(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);
Expand Down
23 changes: 22 additions & 1 deletion src/Sarif.Multitool.Library/SuppressOptions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,16 +29,34 @@ 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<string> 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.")]
public bool Timestamps { get; set; }

[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.")]
Expand Down
11 changes: 9 additions & 2 deletions src/Sarif/Core/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/Sarif/Query/Evaluators/SarifEvaluators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public static IExpressionEvaluator<Result> ResultEvaluator(TermExpression term)
return new DoubleEvaluator<Result>(r => r.Rank, term);
case "ruleid":
return new StringEvaluator<Result>(r => r.GetRule(r.Run).Id, term, StringComparison.OrdinalIgnoreCase);
case "issuppressed":
return new BoolEvaluator<Result>(r => r.TryIsSuppressed(out bool suppressed) && suppressed, term);

case "uri":
// Ensure the Run is provided, to look up Uri from Run.Artifacts when needed.
Expand Down
46 changes: 37 additions & 9 deletions src/Sarif/Visitors/SuppressVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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<string> 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)
Expand All @@ -41,6 +46,13 @@ public override Result VisitResult(Result node)
node.Suppressions = new List<Suppression>();
}

// 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,
Expand All @@ -53,7 +65,7 @@ public override Result VisitResult(Result node)
suppression.SetProperty(nameof(alias), alias);
}

if (guids)
if (this.uuids)
{
suppression.Guid = Guid.NewGuid();
}
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,13 @@ private Run CreateTestRun(int numberOfResult, bool createSubRule = false, string
run.Results ??= new List<Result>();

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<string> { ruleId }, new List<Uri> { artifactUri }, 1));
RandomSarifLogGenerator.GenerateFakeResults(this.random, new List<string> { ruleId }, new List<string>() { guid }, new List<Uri> { artifactUri }, 1));
}

return run;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down
Loading