diff --git a/docs/query-mode.md b/docs/query-mode.md index a3e486746..9cebdb051 100644 --- a/docs/query-mode.md +++ b/docs/query-mode.md @@ -47,10 +47,13 @@ NOT RuleId = SM00251 OR OccurrenceCount > 10 AND OccurrenceCount < 100 * CorrelationGuid * Guid * HostedViewerUri +* IsSuppressed * Kind * Level * Message.Text * OccurrenceCount +* properties.[value] * Rank +* rule.properties.[value] * RuleId * Uri \ No newline at end of file diff --git a/src/Sarif/ExtensionMethods.cs b/src/Sarif/ExtensionMethods.cs index bf0ca6658..c9e16ca71 100644 --- a/src/Sarif/ExtensionMethods.cs +++ b/src/Sarif/ExtensionMethods.cs @@ -288,21 +288,31 @@ public static string FormatForVisualStudio(this Result result, ReportingDescript var messageLines = new List(); - foreach (Location location in result.Locations) + var ruleMessage = string.Format( + CultureInfo.InvariantCulture, "{0} {1}: {2}", + result.Kind == ResultKind.Fail ? result.Level.FormatForVisualStudio() : result.Kind.FormatForVisualStudio(), + result.RuleId, + result.GetMessageText(rule) + ); + + if (result.Locations != null) { - Uri uri = location.PhysicalLocation.ArtifactLocation.Uri; - string path = uri.IsAbsoluteUri && uri.IsFile ? uri.LocalPath : uri.ToString(); - messageLines.Add( - string.Format( - CultureInfo.InvariantCulture, "{0}{1}: {2} {3}: {4}", + foreach (Location location in result.Locations) + { + Uri uri = location.PhysicalLocation.ArtifactLocation.Uri; + string path = uri.IsAbsoluteUri && uri.IsFile ? uri.LocalPath : uri.ToString(); + + ruleMessage = string.Format( + CultureInfo.InvariantCulture, "{0}{1}: {2}", path, location.PhysicalLocation.Region.FormatForVisualStudio(), - result.Kind == ResultKind.Fail ? result.Level.FormatForVisualStudio() : result.Kind.FormatForVisualStudio(), - result.RuleId, - result.GetMessageText(rule) - )); + ruleMessage + ); + } } + messageLines.Add(ruleMessage); + return string.Join(Environment.NewLine, messageLines); } diff --git a/src/Sarif/Query/Evaluators/DateTimeEvaluator.cs b/src/Sarif/Query/Evaluators/DateTimeEvaluator.cs new file mode 100644 index 000000000..7c7b204a6 --- /dev/null +++ b/src/Sarif/Query/Evaluators/DateTimeEvaluator.cs @@ -0,0 +1,124 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Sarif.Query.Evaluators +{ + /// + /// DateTimeEvaluator implements IExpressionEvaluator given a getter which can + /// get the desired Property Name as a DateTime. + /// + /// Usage: + /// if (String.Equals(term.PropertyName, "ID", StringComparison.OrdinalIgnoreCase)) + /// { + /// // Show the DateTimeEvaluator how to get the 'Timestamp' property as a DateTime, and it'll implement the term matching. + /// return new DateTimeEvaluator<Result>(result => result.ID, term); + /// } + /// + /// Type of Item Evaluator will evaluate. + public class DateTimeEvaluator : IExpressionEvaluator + { + private readonly Func _getter; + private readonly DateTime _value; + private readonly Action, BitArray> _evaluateSet; + + public DateTimeEvaluator(Func getter, TermExpression term) + { + _getter = getter; + + if (!DateTime.TryParse(term.Value, out DateTime parsedValue)) { throw new QueryParseException($"{term} value {term.Value} was not a valid DateTime format."); } + _value = parsedValue; + + _evaluateSet = Comparer(term); + } + + public void Evaluate(ICollection list, BitArray matches) + { + _evaluateSet(list, matches); + } + + private Action, BitArray> Comparer(TermExpression term) + { + switch (term.Operator) + { + case CompareOperator.Equals: + return EvaluateEquals; + case CompareOperator.NotEquals: + return EvaluateNotEquals; + case CompareOperator.LessThan: + return EvaluateLessThan; + case CompareOperator.LessThanOrEquals: + return EvaluateLessThanOrEquals; + case CompareOperator.GreaterThan: + return EvaluateGreaterThan; + case CompareOperator.GreaterThanOrEquals: + return EvaluateGreaterThanOrEquals; + default: + throw new QueryParseException($"{term} does not support operator {term.Operator}"); + } + } + + private void EvaluateEquals(ICollection list, BitArray matches) + { + int i = 0; + foreach (T item in list) + { + matches.Set(i, _getter(item).Equals(_value)); + i++; + } + } + + private void EvaluateNotEquals(ICollection list, BitArray matches) + { + int i = 0; + foreach (T item in list) + { + matches.Set(i, !_getter(item).Equals(_value)); + i++; + } + } + + private void EvaluateLessThan(ICollection list, BitArray matches) + { + int i = 0; + foreach (T item in list) + { + matches.Set(i, _getter(item) < _value); + i++; + } + } + + private void EvaluateLessThanOrEquals(ICollection list, BitArray matches) + { + int i = 0; + foreach (T item in list) + { + matches.Set(i, _getter(item) <= _value); + i++; + } + } + + private void EvaluateGreaterThan(ICollection list, BitArray matches) + { + int i = 0; + foreach (T item in list) + { + matches.Set(i, _getter(item) > _value); + i++; + } + } + + private void EvaluateGreaterThanOrEquals(ICollection list, BitArray matches) + { + int i = 0; + foreach (T item in list) + { + matches.Set(i, _getter(item) >= _value); + i++; + } + } + } +} diff --git a/src/Sarif/Query/Evaluators/EvaluatorFactory.cs b/src/Sarif/Query/Evaluators/EvaluatorFactory.cs index 3fc495456..3489362d1 100644 --- a/src/Sarif/Query/Evaluators/EvaluatorFactory.cs +++ b/src/Sarif/Query/Evaluators/EvaluatorFactory.cs @@ -68,6 +68,10 @@ public static object BuildPrimitiveEvaluator(Type fieldType, TermExpression term { return new LongEvaluator(value => value, term); } + else if (fieldType == typeof(DateTime)) + { + return new DateTimeEvaluator(value => value, term); + } else if (fieldType == typeof(string)) { // Default StringComparison only diff --git a/src/Sarif/Query/Evaluators/PropertyBagPropertyEvaluator.cs b/src/Sarif/Query/Evaluators/PropertyBagPropertyEvaluator.cs index ab5b3b103..0348b6e1f 100644 --- a/src/Sarif/Query/Evaluators/PropertyBagPropertyEvaluator.cs +++ b/src/Sarif/Query/Evaluators/PropertyBagPropertyEvaluator.cs @@ -58,10 +58,17 @@ public PropertyBagPropertyEvaluator(TermExpression term) // and numbers. If the value being compared parses as a number, assume that a numeric // comparison was intended, and create a numeric evaluator. Otherwise, create a string evaluator. // This could cause problems if the comparand is string that happens to look like a number. - private IExpressionEvaluator CreateEvaluator(TermExpression term) => - IsStringComparison(term) - ? new StringEvaluator(GetProperty, term, StringComparison.OrdinalIgnoreCase) as IExpressionEvaluator - : new DoubleEvaluator(GetProperty, term); + private IExpressionEvaluator CreateEvaluator(TermExpression term) + { + if (IsStringComparison(term)) + return new StringEvaluator(GetProperty, term, StringComparison.OrdinalIgnoreCase); + else if (IsDateTimeComparison(term)) + return new DateTimeEvaluator(GetProperty, term); + else if (IsDoubleComparison(term)) + return new DoubleEvaluator(GetProperty, term); + else + return new StringEvaluator(GetProperty, term, StringComparison.OrdinalIgnoreCase); + } private static readonly ReadOnlyCollection s_stringSpecificOperators = new ReadOnlyCollection( @@ -73,7 +80,13 @@ private IExpressionEvaluator CreateEvaluator(TermExpression term) => }); private bool IsStringComparison(TermExpression term) - => s_stringSpecificOperators.Contains(term.Operator) || !double.TryParse(term.Value, out _); + => s_stringSpecificOperators.Contains(term.Operator); + + private bool IsDoubleComparison(TermExpression term) + => double.TryParse(term.Value, out _); + + private bool IsDateTimeComparison(TermExpression term) + => DateTime.TryParse(term.Value, out _); private T GetProperty(Result result) { diff --git a/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs b/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs index 2dfce1018..fd2d6e0c5 100644 --- a/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs +++ b/src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs @@ -51,6 +51,38 @@ public void QueryCommand_Basics() Assert.Equal(expected, actual); } + [Fact] + public void QueryCommand_Properties() + { + string filePath = "WithProperties.sarif"; + File.WriteAllText(filePath, s_extractor.GetResourceText(filePath)); + + // rule filter: string + RunAndVerifyCount(1, new QueryOptions() { Expression = "rule.properties.cwe == 'CWE-755'", InputFilePath = filePath }); + + // rule filter: double + RunAndVerifyCount(1, new QueryOptions() { Expression = "rule.properties.security-severity > 7.0", InputFilePath = filePath }); + + // rule filter: date time + RunAndVerifyCount(1, new QueryOptions() { Expression = "rule.properties.vulnPublicationDate > '2022-04-01T00:00:00'", InputFilePath = filePath }); + + // rule filter: date time + RunAndVerifyCount(2, new QueryOptions() { Expression = "rule.properties.vulnPublicationDate < '2022-04-30T00:00:00'", InputFilePath = filePath }); + + // result filter: string + RunAndVerifyCount(1, new QueryOptions() { Expression = "properties.packageManager == 'nuget'", InputFilePath = filePath }); + + // result filter: double + RunAndVerifyCount(1, new QueryOptions() { Expression = "properties.packageManager == 'npm' && properties.severity > 3.0", InputFilePath = filePath }); + + // result filter: date time + RunAndVerifyCount(2, new QueryOptions() { Expression = "properties.patchPublicationDate > '2022-06-01T00:00:00'", InputFilePath = filePath }); + + // result filter: date time + RunAndVerifyCount(0, new QueryOptions() { Expression = "properties.patchPublicationDate < '2022-04-25T00:00:00'", InputFilePath = filePath }); + } + + private void RunAndVerifyCount(int expectedCount, QueryOptions options) { options.ReturnCount = true; diff --git a/src/Test.UnitTests.Sarif.Multitool.Library/Test.UnitTests.Sarif.Multitool.Library.csproj b/src/Test.UnitTests.Sarif.Multitool.Library/Test.UnitTests.Sarif.Multitool.Library.csproj index 0bbbfcf21..d02e51e59 100644 --- a/src/Test.UnitTests.Sarif.Multitool.Library/Test.UnitTests.Sarif.Multitool.Library.csproj +++ b/src/Test.UnitTests.Sarif.Multitool.Library/Test.UnitTests.Sarif.Multitool.Library.csproj @@ -19,7 +19,7 @@ - + @@ -54,6 +54,7 @@ + @@ -86,4 +87,4 @@ - + \ No newline at end of file diff --git a/src/Test.UnitTests.Sarif.Multitool.Library/TestData/QueryCommand/WithProperties.sarif b/src/Test.UnitTests.Sarif.Multitool.Library/TestData/QueryCommand/WithProperties.sarif new file mode 100644 index 000000000..b9fe87d7a --- /dev/null +++ b/src/Test.UnitTests.Sarif.Multitool.Library/TestData/QueryCommand/WithProperties.sarif @@ -0,0 +1,77 @@ +{ + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.1", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Test", + "version": "1.0.0", + "rules": [ + { + "id": "TEST0001", + "name": "Test", + "shortDescription": { + "text": "Test description." + }, + "messageStrings": { + "default": { + "text": "Test description." + } + }, + "properties": { + "cwe": "CWE-755", + "security-severity": 8.0, + "vulnPublicationDate": "2022-04-24T10:58:25Z" + } + }, + { + "id": "TEST0002", + "name": "Test 2", + "shortDescription": { + "text": "Test description 2." + }, + "messageStrings": { + "default": { + "text": "Test description 2." + } + }, + "properties": { + "cwe": "CWE-766", + "security-severity": 3.0, + "vulnPublicationDate": "2022-01-15T10:58:25Z" + } + } + ] + } + }, + "results": [ + { + "ruleId": "TEST0001", + "ruleIndex": 0, + "message": { + "text": "Test text." + }, + "properties": { + "packageManager": "nuget", + "severity": 2.0, + "patchPublicationDate": "2022-06-01T10:58:25Z" + } + }, + { + "ruleId": "TEST0002", + "ruleIndex": 1, + "message": { + "text": "Test text 2." + }, + "properties": { + "packageManager": "npm", + "severity": 9.0, + "patchPublicationDate": "2022-10-01T10:58:25Z" + } + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/src/Test.UnitTests.Sarif.Multitool/QueryCommandTests.cs b/src/Test.UnitTests.Sarif.Multitool/QueryCommandTests.cs index ebe75c53e..7b6cef7e0 100644 --- a/src/Test.UnitTests.Sarif.Multitool/QueryCommandTests.cs +++ b/src/Test.UnitTests.Sarif.Multitool/QueryCommandTests.cs @@ -62,6 +62,13 @@ public void QueryCommand_PerformsStringSpecificComparisons() RunAndVerifyCount(1, "properties.confidence >| 9"); // endswith } + [Fact] + public void QueryCommand_PerformsDateTimeSpecificComparisons() + { + RunAndVerifyCount(1, "rule.properties.publishDate > '2022-10-01' && rule.properties.publishDate < '2022-10-02'"); // result + RunAndVerifyCount(1, "properties.publishDate > '2022-10-28' && properties.publishDate < '2022-10-29'"); // rule + } + [Fact] public void QueryCommand_TreatsUnparseableValueAsHavingTheDefaultValue() { diff --git a/src/Test.UnitTests.Sarif.Multitool/TestData/QueryCommand/property-bag-queries.sarif b/src/Test.UnitTests.Sarif.Multitool/TestData/QueryCommand/property-bag-queries.sarif index 1844c621e..254cf0a87 100644 --- a/src/Test.UnitTests.Sarif.Multitool/TestData/QueryCommand/property-bag-queries.sarif +++ b/src/Test.UnitTests.Sarif.Multitool/TestData/QueryCommand/property-bag-queries.sarif @@ -15,7 +15,8 @@ "id": "TEST1002", "name": "PropertyBagWithNoMatchingProperty", "properties": { - "someOtherProperty": "x" + "someOtherProperty": "x", + "publishDate": "2022-10-01T13:01:42" } }, { @@ -51,7 +52,8 @@ }, "properties": { "comment": "Exact match.", - "name": "Calliope" + "name": "Calliope", + "publishDate": "2022-10-28T16:53:24" } }, {