Skip to content

Commit

Permalink
FEATURE: #2560 DateTime Query Evaluator #2561 (#2562) (#2563)
Browse files Browse the repository at this point in the history
* FEATURE: #2560 DateTime Query Evaluator #2561 (#2562)

* chore: query docs

* feat: date time property evaluator

* bug: null location guard check

* feat: enhanced query command test suite

* chore: formatting failure

* chore: codeql warning

* bug: fixed failing string op test case

* feat: date time test cases

Co-authored-by: Michael C. Fanning <mikefan@microsoft.com>
  • Loading branch information
ejohn20 and michaelcfanning authored Nov 7, 2022
1 parent d73a262 commit 31f49b2
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 19 deletions.
3 changes: 3 additions & 0 deletions docs/query-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 20 additions & 10 deletions src/Sarif/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,21 +288,31 @@ public static string FormatForVisualStudio(this Result result, ReportingDescript

var messageLines = new List<string>();

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);
}

Expand Down
124 changes: 124 additions & 0 deletions src/Sarif/Query/Evaluators/DateTimeEvaluator.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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&lt;Result&gt;(result => result.ID, term);
/// }
/// </summary>
/// <typeparam name="T">Type of Item Evaluator will evaluate.</typeparam>
public class DateTimeEvaluator<T> : IExpressionEvaluator<T>
{
private readonly Func<T, DateTime> _getter;
private readonly DateTime _value;
private readonly Action<ICollection<T>, BitArray> _evaluateSet;

public DateTimeEvaluator(Func<T, DateTime> 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<T> list, BitArray matches)
{
_evaluateSet(list, matches);
}

private Action<ICollection<T>, 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<T> list, BitArray matches)
{
int i = 0;
foreach (T item in list)
{
matches.Set(i, _getter(item).Equals(_value));
i++;
}
}

private void EvaluateNotEquals(ICollection<T> list, BitArray matches)
{
int i = 0;
foreach (T item in list)
{
matches.Set(i, !_getter(item).Equals(_value));
i++;
}
}

private void EvaluateLessThan(ICollection<T> list, BitArray matches)
{
int i = 0;
foreach (T item in list)
{
matches.Set(i, _getter(item) < _value);
i++;
}
}

private void EvaluateLessThanOrEquals(ICollection<T> list, BitArray matches)
{
int i = 0;
foreach (T item in list)
{
matches.Set(i, _getter(item) <= _value);
i++;
}
}

private void EvaluateGreaterThan(ICollection<T> list, BitArray matches)
{
int i = 0;
foreach (T item in list)
{
matches.Set(i, _getter(item) > _value);
i++;
}
}

private void EvaluateGreaterThanOrEquals(ICollection<T> list, BitArray matches)
{
int i = 0;
foreach (T item in list)
{
matches.Set(i, _getter(item) >= _value);
i++;
}
}
}
}
4 changes: 4 additions & 0 deletions src/Sarif/Query/Evaluators/EvaluatorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public static object BuildPrimitiveEvaluator(Type fieldType, TermExpression term
{
return new LongEvaluator<sbyte>(value => value, term);
}
else if (fieldType == typeof(DateTime))
{
return new DateTimeEvaluator<DateTime>(value => value, term);
}
else if (fieldType == typeof(string))
{
// Default StringComparison only
Expand Down
23 changes: 18 additions & 5 deletions src/Sarif/Query/Evaluators/PropertyBagPropertyEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result> CreateEvaluator(TermExpression term) =>
IsStringComparison(term)
? new StringEvaluator<Result>(GetProperty<string>, term, StringComparison.OrdinalIgnoreCase) as IExpressionEvaluator<Result>
: new DoubleEvaluator<Result>(GetProperty<double>, term);
private IExpressionEvaluator<Result> CreateEvaluator(TermExpression term)
{
if (IsStringComparison(term))
return new StringEvaluator<Result>(GetProperty<string>, term, StringComparison.OrdinalIgnoreCase);
else if (IsDateTimeComparison(term))
return new DateTimeEvaluator<Result>(GetProperty<DateTime>, term);
else if (IsDoubleComparison(term))
return new DoubleEvaluator<Result>(GetProperty<double>, term);
else
return new StringEvaluator<Result>(GetProperty<string>, term, StringComparison.OrdinalIgnoreCase);
}

private static readonly ReadOnlyCollection<CompareOperator> s_stringSpecificOperators =
new ReadOnlyCollection<CompareOperator>(
Expand All @@ -73,7 +80,13 @@ private IExpressionEvaluator<Result> 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<T>(Result result)
{
Expand Down
32 changes: 32 additions & 0 deletions src/Test.UnitTests.Sarif.Multitool.Library/QueryCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<EmbeddedResource Remove="TestData\ExportRuleDocumentationCommand\**" />
<None Remove="TestData\ExportRuleDocumentationCommand\**" />
</ItemGroup>

<ItemGroup>
<None Remove="TestData\ExportValidationRulesMetadataCommand\ExpectedOutputs\MarkdownFullDescription.md" />
<None Remove="TestData\ExportValidationRulesMetadataCommand\ExpectedOutputs\MarkdownShortDescription.md" />
Expand Down Expand Up @@ -54,6 +54,7 @@
<EmbeddedResource Include="TestData\QueryCommand\elfie-arriba.CSCAN0020.sarif" />
<EmbeddedResource Include="TestData\PageCommand\elfie-arriba.sarif" />
<EmbeddedResource Include="TestData\QueryCommand\elfie-arriba.sarif" />
<EmbeddedResource Include="TestData\QueryCommand\WithProperties.sarif" />
<EmbeddedResource Include="TestData\RebaseUriCommand\ExpectedOutputs\RunWithArtifacts.sarif" />
<EmbeddedResource Include="TestData\RebaseUriCommand\Inputs\RunWithArtifacts.sarif" />
<EmbeddedResource Include="TestData\ValidateCommand\Configuration.json" />
Expand Down Expand Up @@ -86,4 +87,4 @@
<ProjectReference Include="..\Sarif.Multitool.Library\Sarif.Multitool.Library.csproj" />
<ProjectReference Include="..\Test.Utilities.Sarif\Test.Utilities.Sarif.csproj" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
7 changes: 7 additions & 0 deletions src/Test.UnitTests.Sarif.Multitool/QueryCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading

0 comments on commit 31f49b2

Please sign in to comment.