Adds DependsOnTags field to Rule (#533)

Fix #533 

* Adds DependsOnTags field to Rule

After processing of rules DependsOnTags will be checked and then matches whose matching rule have required tags which are not all present in the UniqueTags will be removed before returning results. Adds tests for new functionality and rule verifier will check to make sure all tags which are depended on are present in the rule set.

SemVer changes. Public properties in the Rule object have been changed to IList from a combination of Array and List.

* Fix query for rules with null depends on field

* Formatting: Add Missing Braces Back

* Revert "Formatting: Add Missing Braces Back"

This reverts commit d6dc1fc5a7.

* Set beta flag

* Fix Verification message for missing depends_on_tags

Change a few more List to IList.
Improve edge case in verification where if two rules shared the same id they would both receive the error message about depends_on_tags.

* Improve description for test rules

* Formatting and additional comments on the Rule and AnalyzeCommand objects

* Catch edge case in verification where an overridden rule doesn't have the depends on of its overrider

This verification step prevents a potential issue if a ruleset contains Rule A, which is Overridden by Rule B which depends on TagX.

If RuleA and RuleB match, but TagX is not present, no results would be returned. This is because overrides are performed on a file by file basis, as the last step in processing each file. Depends on tags are checked after all files have been processed, and so the overridden matches are no longer tracked.

* Adds Support for Chained Dependent Rules

And tests for same.

* Typo in test rule description

* Small refactor to deduplicate logic

* Improve variable names
This commit is contained in:
Gabe Stocco 2023-03-07 12:32:59 -08:00 коммит произвёл GitHub
Родитель ec40f38f8b
Коммит 60f166d8cf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 888 добавлений и 173 удалений

Просмотреть файл

@ -19,7 +19,7 @@ public class DistinctBenchmarks
List<string> outList = new();
foreach (var r in ruleSet)
//builds a list of unique tags
foreach (var t in r?.Tags ?? Array.Empty<string>())
foreach (var t in (IList<string>?)r?.Tags ?? Array.Empty<string>())
if (uniqueTags.ContainsKey(t))
{
continue;
@ -41,7 +41,7 @@ public class DistinctBenchmarks
HashSet<string> hashSet = new();
foreach (var r in ruleSet)
//builds a list of unique tags
foreach (var t in r?.Tags ?? Array.Empty<string>())
foreach (var t in (IList<string>?)r?.Tags ?? Array.Empty<string>())
hashSet.Add(t);
var theList = hashSet.ToList();
@ -52,14 +52,14 @@ public class DistinctBenchmarks
[Benchmark]
public List<string> WithLinq()
{
return ruleSet.SelectMany(x => x.Tags ?? Array.Empty<string>()).Distinct().OrderBy(x => x)
return ruleSet.SelectMany(x => (IList<string>?)x.Tags ?? Array.Empty<string>()).Distinct().OrderBy(x => x)
.ToList();
}
[Benchmark]
public List<string> WithLinqAndHashSet()
{
var theList = ruleSet.SelectMany(x => x.Tags ?? Array.Empty<string>()).ToHashSet().ToList();
var theList = ruleSet.SelectMany(x => (IList<string>?)x.Tags ?? Array.Empty<string>()).ToHashSet().ToList();
theList.Sort();
return theList;
}

Просмотреть файл

@ -77,7 +77,9 @@ public static class MsgHelp
BROWSER_START_SUCCESS,
PACK_MISSING_OUTPUT_ARG,
PACK_RULES_NO_CLI_DEFAULT,
PACK_RULES_NO_DEFAULT
PACK_RULES_NO_DEFAULT,
VERIFY_RULES_DEPENDS_ON_TAG_MISSING,
VERIFY_RULES_OVERRIDDEN_RULE_DEPENDS_ON_TAG_MISSING
}
public static string GetString(ID id)

19
AppInspector.Common/Properties/Resources.Designer.cs сгенерированный
Просмотреть файл

@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@ -583,6 +584,15 @@ namespace Microsoft.ApplicationInspector.Common.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Rule {0} failed verification. depends_on_tags is set but the the following tags do not exist in the RuleSet: {1}.
/// </summary>
internal static string VERIFY_RULES_DEPENDS_ON_TAG_MISSING {
get {
return ResourceManager.GetString("VERIFY_RULES_DEPENDS_ON_TAG_MISSING", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Rule {0} failed from dupicate rule id specified.
/// </summary>
@ -619,6 +629,15 @@ namespace Microsoft.ApplicationInspector.Common.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Rule {0} failed verification. When a rule is overridden it must have all the depends_on_tags of the overridder, these tags are missing: {1}.
/// </summary>
internal static string VERIFY_RULES_OVERRIDDEN_RULE_DEPENDS_ON_TAG_MISSING {
get {
return ResourceManager.GetString("VERIFY_RULES_OVERRIDDEN_RULE_DEPENDS_ON_TAG_MISSING", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Rule {0} failed from invalid regex &apos;{1}&apos; with {2}.
/// </summary>

Просмотреть файл

@ -302,7 +302,13 @@
<data name="VERIFY_RULES_DUPLICATEID_FAIL" xml:space="preserve">
<value>Rule {0} failed from dupicate rule id specified</value>
</data>
<data name="VERIFY_RULES_LANGUAGE_FAIL" xml:space="preserve">
<data name="VERIFY_RULES_DEPENDS_ON_TAG_MISSING" xml:space="preserve">
<value>Rule {0} failed verification. depends_on_tags is set but the the following tags do not exist in the RuleSet: {1}</value>
</data>
<data name="VERIFY_RULES_OVERRIDDEN_RULE_DEPENDS_ON_TAG_MISSING" xml:space="preserve">
<value>Rule {0} failed verification. When a rule is overridden it must have all the depends_on_tags of the overridder, these tags are missing: {1}</value>
</data>
<data name="VERIFY_RULES_LANGUAGE_FAIL" xml:space="preserve">
<value>Rule {0} failed from unrecognized language {1} specified</value>
</data>
<data name="VERIFY_RULES_NO_CLI_DEFAULT" xml:space="preserve">

Просмотреть файл

@ -63,8 +63,8 @@ public abstract class AbstractRuleSet
public IEnumerable<ConvertedOatRule> GetUniversalRules()
{
return _oatRules.Where(x =>
(x.AppInspectorRule.FileRegexes is null || x.AppInspectorRule.FileRegexes.Length == 0) &&
(x.AppInspectorRule.AppliesTo is null || x.AppInspectorRule.AppliesTo.Length == 0));
(x.AppInspectorRule.FileRegexes is null || x.AppInspectorRule.FileRegexes.Count == 0) &&
(x.AppInspectorRule.AppliesTo is null || x.AppInspectorRule.AppliesTo.Count == 0));
}
/// <summary>

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
namespace Microsoft.ApplicationInspector.RulesEngine;
@ -21,7 +22,7 @@ public class MatchRecord
RuleId = rule.Id;
RuleName = rule.Name;
RuleDescription = rule.Description;
Tags = rule.Tags;
Tags = rule.Tags?.ToArray();
Severity = rule.Severity;
}

Просмотреть файл

@ -1,100 +1,152 @@
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace Microsoft.ApplicationInspector.RulesEngine;
/// <summary>
/// Class to hold the Rule
/// </summary>
public class Rule
namespace Microsoft.ApplicationInspector.RulesEngine
{
private IEnumerable<Regex> _compiled = Array.Empty<Regex>();
private string[]? _fileRegexes;
private bool _updateCompiledFileRegex;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
/// <summary>
/// Name of the source where the rule definition came from.
/// Typically file, database or other storage.
/// Class to hold the Rule
/// </summary>
[JsonIgnore]
public string? Source { get; set; }
/// <summary>
/// Optional tag assigned to the rule during runtime
/// </summary>
[JsonIgnore]
public string? RuntimeTag { get; set; }
/// <summary>
/// Runtime flag to disable the rule
/// </summary>
[JsonIgnore]
public bool Disabled { get; set; }
[JsonPropertyName("name")] public string Name { get; set; } = "";
[JsonPropertyName("id")] public string Id { get; set; } = "";
[JsonPropertyName("description")]
public string? Description { get; set; } = "";
[JsonPropertyName("does_not_apply_to")]
public List<string>? DoesNotApplyTo { get; set; }
[JsonPropertyName("applies_to")]
public string[]? AppliesTo { get; set; }
[JsonPropertyName("applies_to_file_regex")]
public string[]? FileRegexes
public class Rule
{
get => _fileRegexes;
set
{
_fileRegexes = value;
_updateCompiledFileRegex = true;
}
}
private IList<Regex> _compiled = Array.Empty<Regex>();
[JsonIgnore]
public IEnumerable<Regex> CompiledFileRegexes
{
get
private IList<string>? _fileRegexes;
private bool _updateCompiledFileRegex;
/// <summary>
/// Name of the source where the rule definition came from.
/// Typically file, database or other storage.
/// </summary>
[JsonIgnore]
public string? Source { get; set; }
/// <summary>
/// Optional tag assigned to the rule during runtime
/// </summary>
[JsonIgnore]
public string? RuntimeTag { get; set; }
/// <summary>
/// Runtime flag to disable the rule
/// </summary>
[JsonIgnore]
public bool Disabled { get; set; }
/// <summary>
/// Tags that are required to be present in the total result set - even from other rules matched against other files - for this match to be valid
/// Does not work with `TagsOnly` option.
/// </summary>
[JsonPropertyName("depends_on_tags")] public IList<string>? DependsOnTags { get; set; }
/// <summary>
/// Human readable name for the rule
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = "";
/// <summary>
/// Id for the rule, by default IDs must be unique.
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; } = "";
/// <summary>
/// Human readable description for the rule
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; set; } = "";
/// <summary>
/// Languages that the rule does not apply to
/// </summary>
[JsonPropertyName("does_not_apply_to")]
public IList<string>? DoesNotApplyTo { get; set; }
/// <summary>
/// Languages that the rule does apply to, if empty, applies to all languages not in <see cref="DoesNotApplyTo"/>
/// </summary>
[JsonPropertyName("applies_to")]
public IList<string>? AppliesTo { get; set; }
/// <summary>
/// Regular expressions for file names that the Rule applies to
/// </summary>
[JsonPropertyName("applies_to_file_regex")]
public IList<string>? FileRegexes
{
if (_updateCompiledFileRegex)
get => _fileRegexes;
set
{
_compiled = FileRegexes?.Select(x => new Regex(x, RegexOptions.Compiled)) ?? Array.Empty<Regex>();
_updateCompiledFileRegex = false;
_fileRegexes = value;
_updateCompiledFileRegex = true;
}
return _compiled;
}
/// <summary>
/// Internal API to cache construction of <see cref="FileRegexes"/>
/// </summary>
[JsonIgnore]
internal IEnumerable<Regex> CompiledFileRegexes
{
get
{
if (_updateCompiledFileRegex)
{
_compiled = (IList<Regex>?)FileRegexes?.Select(x => new Regex(x, RegexOptions.Compiled)).ToList() ?? Array.Empty<Regex>();
_updateCompiledFileRegex = false;
}
return _compiled;
}
}
/// <summary>
/// The Tags the rule provides
/// </summary>
[JsonPropertyName("tags")]
public IList<string>? Tags { get; set; }
/// <summary>
/// Severity for the rule
/// </summary>
[JsonPropertyName("severity")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public Severity Severity { get; set; } = Severity.Moderate;
/// <summary>
/// Other rules that this rule overrides
/// </summary>
[JsonPropertyName("overrides")]
public IList<string>? Overrides { get; set; }
/// <summary>
/// When any pattern matches, the rule applies
/// </summary>
[JsonPropertyName("patterns")]
public SearchPattern[] Patterns { get; set; } = Array.Empty<SearchPattern>();
/// <summary>
/// If any patterns match, and any conditions are set, all conditions must also match
/// </summary>
[JsonPropertyName("conditions")]
public SearchCondition[]? Conditions { get; set; }
/// <summary>
/// Optional list of self-test sample texts that the rule must match to be considered valid.
/// </summary>
[JsonPropertyName("must-match")]
public IList<string>? MustMatch { get; set; }
/// <summary>
/// Optional list of self-test sample texts that the rule must not match to be considered valid.
/// </summary>
[JsonPropertyName("must-not-match")]
public IList<string>? MustNotMatch { get; set; }
}
[JsonPropertyName("tags")] public string[]? Tags { get; set; }
[JsonPropertyName("severity")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public Severity Severity { get; set; } = Severity.Moderate;
[JsonPropertyName("overrides")]
public string[]? Overrides { get; set; }
[JsonPropertyName("patterns")]
public SearchPattern[] Patterns { get; set; } = Array.Empty<SearchPattern>();
[JsonPropertyName("conditions")]
public SearchCondition[]? Conditions { get; set; }
[JsonPropertyName("must-match")]
public string[]? MustMatch { get; set; }
[JsonPropertyName("must-not-match")]
public string[]? MustNotMatch { get; set; }
}

Просмотреть файл

@ -211,7 +211,9 @@ public class RuleProcessor
// WithinClauses are always ANDed, but each contains all the captures that passed *that* clause.
// We need the captures that passed every clause.
foreach (var aCapture in allCaptured)
{
numberOfInstances.AddOrUpdate(aCapture, 1, (tuple, i) => i + 1);
}
return numberOfInstances.Where(x => x.Value == onlyWithinCaptures.Count).Select(x => x.Key)
.ToList();
}
@ -229,16 +231,22 @@ public class RuleProcessor
List<MatchRecord> removes = new();
foreach (var m in resultsList.Where(x => x.Rule?.Overrides?.Length > 0))
foreach (var idsToOverride in m.Rule?.Overrides ?? Array.Empty<string>())
// Find all overriden rules and mark them for removal from issues list
foreach (var om in resultsList.FindAll(x => x.Rule?.Id == idsToOverride))
// If the overridden match is a subset of the overriding match
if (om.Boundary.Index >= m.Boundary.Index &&
om.Boundary.Index <= m.Boundary.Index + m.Boundary.Length)
foreach (var m in resultsList.Where(x => x.Rule?.Overrides?.Count > 0))
{
foreach (var idsToOverride in m.Rule?.Overrides ?? Array.Empty<string>())
{
removes.Add(om);
// Find all overriden rules and mark them for removal from issues list
foreach (var om in resultsList.FindAll(x => x.Rule?.Id == idsToOverride))
{
// If the overridden match is a subset of the overriding match
if (om.Boundary.Index >= m.Boundary.Index &&
om.Boundary.Index <= m.Boundary.Index + m.Boundary.Length)
{
removes.Add(om);
}
}
}
}
// Remove overriden rules
resultsList.RemoveAll(x => removes.Contains(x));
@ -416,14 +424,14 @@ public class RuleProcessor
List<MatchRecord> removes = new();
foreach (var m in resultsList.Where(x => x.Rule?.Overrides?.Length > 0))
foreach (var m in resultsList.Where(x => x.Rule?.Overrides?.Count > 0))
{
if (cancellationToken?.IsCancellationRequested is true)
{
return resultsList;
}
foreach (var ovrd in m.Rule?.Overrides ?? Array.Empty<string>())
foreach (var ovrd in (IList<string>?)m.Rule?.Overrides ?? Array.Empty<string>())
// Find all overriden rules and mark them for removal from issues list
foreach (var om in resultsList.FindAll(x => x.Rule?.Id == ovrd))
if (om.Boundary.Index >= m.Boundary.Index &&

Просмотреть файл

@ -13,4 +13,5 @@ public class RuleStatus
public IEnumerable<Violation> OatIssues { get; set; } = Enumerable.Empty<Violation>();
public bool HasPositiveSelfTests { get; set; }
public bool HasNegativeSelfTests { get; set; }
internal Rule Rule { get; set; }
}

Просмотреть файл

@ -69,7 +69,12 @@ public class RulesVerifier
return new RulesVerifierResult(CheckIntegrity(ruleset), ruleset);
}
public List<RuleStatus> CheckIntegrity(AbstractRuleSet ruleSet)
/// <summary>
/// Check an <see cref="AbstractRuleSet"/> for rules errors
/// </summary>
/// <param name="ruleSet">The rule set to check</param>
/// <returns>An <see cref="IList{RuleStatus}"/> with a <see cref="RuleStatus"/> for each <see cref="Rule"/> in the <paramref name="ruleSet"/></returns>
public IList<RuleStatus> CheckIntegrity(AbstractRuleSet ruleSet)
{
List<RuleStatus> ruleStatuses = new();
foreach (var rule in ruleSet.GetOatRules())
@ -79,6 +84,7 @@ public class RulesVerifier
ruleStatuses.Add(ruleVerified);
}
// By default unique IDs are required for rules
if (!_options.DisableRequireUniqueIds)
{
var duplicatedRules = ruleSet.GetAppInspectorRules().GroupBy(x => x.Id).Where(y => y.Count() > 1);
@ -92,6 +98,42 @@ public class RulesVerifier
}
}
// Check for the presence of the `depends_on` field and ensure that any tags which are depended on exist in the full set of rules
var allTags = ruleSet.GetAppInspectorRules().SelectMany(x => x.Tags ?? Array.Empty<string>()).ToList();
var rulesWithDependsOnWithNoMatchingTags = ruleSet.GetAppInspectorRules().Where(x => !x.DependsOnTags?.All(tag => allTags.Contains(tag)) ?? false);
foreach(var dependslessRule in rulesWithDependsOnWithNoMatchingTags)
{
_logger.LogError(MsgHelp.GetString(MsgHelp.ID.VERIFY_RULES_DEPENDS_ON_TAG_MISSING), dependslessRule.Id, string.Join(',', dependslessRule.DependsOnTags?.Where(tag => !allTags.Contains(tag)) ?? Array.Empty<string>()));
foreach(var status in ruleStatuses.Where(x => x.Rule == dependslessRule))
{
status.Errors = status.Errors.Append(MsgHelp.FormatString(MsgHelp.ID.VERIFY_RULES_DEPENDS_ON_TAG_MISSING, dependslessRule.Id, string.Join(',',dependslessRule.DependsOnTags?.Where(tag => !allTags.Contains(tag)) ?? Array.Empty<string>())));
}
}
// Overrides are removed on a per file basis where depends_on is removed on a cross scan basis. Because of this, if you have RuleA with no DependsOnTags which is overriden with RuleB which does have tags,
// and then those tags are not present, you may expect to get RuleA but will not.
// This checks to ensure if a rule is overridden it has at least all the depends on tags of its overrider
var appInsStyleRules = ruleSet.GetAppInspectorRules();
foreach (var rule in ruleSet.GetAppInspectorRules())
{
foreach(var overrde in rule.Overrides ?? Array.Empty<string>())
{
foreach(var overriddenRule in appInsStyleRules.Where(x => x.Id == overrde))
{
var missingTags = rule.DependsOnTags?.Where(x => !(overriddenRule.DependsOnTags?.Contains(x) ?? false));
if (missingTags?.Any() ?? false)
{
_logger.LogError(MsgHelp.GetString(MsgHelp.ID.VERIFY_RULES_OVERRIDDEN_RULE_DEPENDS_ON_TAG_MISSING), overriddenRule.Id, string.Join(',', missingTags ?? Array.Empty<string>()));
foreach (var status in ruleStatuses.Where(x => x.Rule == overriddenRule))
{
status.Errors = status.Errors.Append(MsgHelp.FormatString(MsgHelp.ID.VERIFY_RULES_OVERRIDDEN_RULE_DEPENDS_ON_TAG_MISSING, overriddenRule.Id, string.Join(',', missingTags ?? Array.Empty<string>())));
}
}
}
}
}
return ruleStatuses;
}
@ -124,7 +166,8 @@ public class RulesVerifier
}
}
foreach (var pattern in rule.FileRegexes ?? Array.Empty<string>())
// Check that regexes for filenames are valid
foreach (var pattern in (IList<string>?)rule.FileRegexes ?? Array.Empty<string>())
try
{
_ = new Regex(pattern, RegexOptions.Compiled);
@ -140,6 +183,7 @@ public class RulesVerifier
//valid search pattern
foreach (var searchPattern in rule.Patterns ?? Array.Empty<SearchPattern>())
{
// Check that pattern regex arguments are valid
if (searchPattern.PatternType == PatternType.RegexWord || searchPattern.PatternType == PatternType.Regex)
{
try
@ -163,6 +207,7 @@ public class RulesVerifier
}
}
// Check that JsonPaths are valid
if (searchPattern.JsonPaths is not null)
{
foreach (var jsonPath in searchPattern.JsonPaths)
@ -182,6 +227,7 @@ public class RulesVerifier
}
}
// Check that XPaths are valid
if (searchPattern.XPaths is not null)
{
foreach (var xpath in searchPattern.XPaths)
@ -199,6 +245,7 @@ public class RulesVerifier
}
}
// Check that YamlPaths are valid
if (searchPattern.YamlPaths is not null)
{
foreach (var yamlPath in searchPattern.YamlPaths)
@ -273,7 +320,7 @@ public class RulesVerifier
StringComparer.InvariantCultureIgnoreCase) ?? true) ?? "csharp";
// validate all must match samples are matched
foreach (var mustMatchElement in rule.MustMatch ?? Array.Empty<string>())
foreach (var mustMatchElement in (IList<string>?)rule.MustMatch ?? Array.Empty<string>())
{
var tc = new TextContainer(mustMatchElement, language, _options.LanguageSpecs);
if (!_analyzer.Analyze(singleList, tc).Any())
@ -285,7 +332,7 @@ public class RulesVerifier
}
// validate no must not match conditions are matched
foreach (var mustNotMatchElement in rule.MustNotMatch ?? Array.Empty<string>())
foreach (var mustNotMatchElement in (IList<string>?)rule.MustNotMatch ?? Array.Empty<string>())
{
var tc = new TextContainer(mustNotMatchElement, language, _options.LanguageSpecs);
if (_analyzer.Analyze(singleList, tc).Any())
@ -296,12 +343,14 @@ public class RulesVerifier
}
}
if (rule.Tags?.Length == 0)
// Check for at least one tag being populated
if ((rule.Tags?.Count ?? 0) == 0)
{
_logger?.LogError("Rule must specify tags. {0}", rule.Id);
errors.Add($"Rule must specify tags. {rule.Id}");
}
// If RequireMustMatch is set every rule must have a must-match self-test
if (_options.RequireMustMatch)
{
if (rule.MustMatch?.Any() is not true)
@ -311,6 +360,7 @@ public class RulesVerifier
}
}
// If RequireMustNotMatch is set every rule must have a must-not-match self-test
if (_options.RequireMustNotMatch)
{
if (rule.MustNotMatch?.Any() is not true)
@ -322,12 +372,13 @@ public class RulesVerifier
return new RuleStatus
{
Rule = rule,
RulesId = rule.Id,
RulesName = rule.Name,
Errors = errors,
OatIssues = _analyzer.EnumerateRuleIssues(convertedOatRule),
HasPositiveSelfTests = rule.MustMatch?.Length > 0,
HasNegativeSelfTests = rule.MustNotMatch?.Length > 0
HasPositiveSelfTests = rule.MustMatch?.Count > 0,
HasNegativeSelfTests = rule.MustNotMatch?.Count > 0
};
}
}

Просмотреть файл

@ -5,13 +5,13 @@ namespace Microsoft.ApplicationInspector.RulesEngine;
public class RulesVerifierResult
{
public RulesVerifierResult(List<RuleStatus> ruleStatuses, AbstractRuleSet compiledRuleSets)
public RulesVerifierResult(IList<RuleStatus> ruleStatuses, AbstractRuleSet compiledRuleSets)
{
RuleStatuses = ruleStatuses;
CompiledRuleSet = compiledRuleSets;
}
public List<RuleStatus> RuleStatuses { get; }
public IList<RuleStatus> RuleStatuses { get; }
public AbstractRuleSet CompiledRuleSet { get; }
public bool Verified => RuleStatuses.All(x => x.Verified);
}

Просмотреть файл

@ -25,6 +25,179 @@ public class TestAnalyzeCmd
private const int numTimeOutFiles = 25;
private const int numTimesContent = 25;
private const string dependsOnChain = @"[
{
""id"": ""SA000001"",
""name"": ""Testing.Rules.DependsOnTags.Chain.A"",
""tags"": [
""Category.A""
],
""severity"": ""Critical"",
""description"": ""This rule finds A"",
""patterns"": [
{
""pattern"": ""A"",
""type"": ""regex"",
""confidence"": ""High"",
""modifiers"": [
""m""
],
""scopes"": [
""code""
]
}
],
""_comment"": """"
},
{
""id"": ""SA000002"",
""name"": ""Testing.Rules.DependsOnTags.Chain.B"",
""tags"": [
""Category.B""
],
""depends_on_tags"": [""Category.A""],
""severity"": ""Critical"",
""description"": ""This rule finds B"",
""patterns"": [
{
""pattern"": ""B"",
""type"": ""regex"",
""confidence"": ""High"",
""modifiers"": [
""m""
],
""scopes"": [
""code""
]
}
],
""_comment"": """"
},
{
""id"": ""SA000003"",
""name"": ""Testing.Rules.DependsOnTags.Chain.C"",
""tags"": [
""Category.C""
],
""depends_on_tags"": [""Category.B""],
""severity"": ""Critical"",
""description"": ""This rule finds C"",
""patterns"": [
{
""pattern"": ""C"",
""type"": ""regex"",
""confidence"": ""High"",
""modifiers"": [
""m""
],
""scopes"": [
""code""
]
}
],
""_comment"": """"
}
]";
private const string dependsOnOneWay = @"[
{
""id"": ""SA000005"",
""name"": ""Testing.Rules.DependsOnTags.OneWay"",
""tags"": [
""Dependant""
],
""depends_on_tags"": [""Dependee""],
""severity"": ""Critical"",
""description"": ""This rule finds windows 2000 and is dependent on the Dependee tag"",
""patterns"": [
{
""pattern"": ""windows 2000"",
""type"": ""regex"",
""confidence"": ""High"",
""modifiers"": [
""m""
],
""scopes"": [
""code""
]
}
],
""_comment"": """"
},
{
""id"": ""SA000006"",
""name"": ""Testing.Rules.DependsOnTags.OneWay"",
""tags"": [
""Dependee""
],
""severity"": ""Critical"",
""description"": ""This rule finds linux and is depended on to provide the Dependee tag"",
""patterns"": [
{
""pattern"": ""linux"",
""type"": ""regex"",
""confidence"": ""High"",
""modifiers"": [
""m""
],
""scopes"": [
""code""
]
}
],
""_comment"": """"
}
]";
private const string dependsOnTwoWay = @"[
{
""id"": ""SA000005"",
""name"": ""Testing.Rules.DependsOnTags.TwoWay"",
""tags"": [
""RuleOne""
],
""depends_on_tags"": [""RuleTwo""],
""severity"": ""Critical"",
""description"": ""This rule finds windows 2000 and is dependent the RuleTwo tag"",
""patterns"": [
{
""pattern"": ""windows 2000"",
""type"": ""regex"",
""confidence"": ""High"",
""modifiers"": [
""m""
],
""scopes"": [
""code""
]
}
],
""_comment"": """"
},
{
""id"": ""SA000006"",
""name"": ""Testing.Rules.DependsOnTags.TwoWay"",
""tags"": [
""RuleTwo""
],
""depends_on_tags"": [""RuleOne""],
""severity"": ""Critical"",
""description"": ""This rule finds linux and is dependent the RuleOne tag"",
""patterns"": [
{
""pattern"": ""linux"",
""type"": ""regex"",
""confidence"": ""High"",
""modifiers"": [
""m""
],
""scopes"": [
""code""
]
}
],
""_comment"": """"
}
]";
private const string hardToFindContent = @"
asefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlakasefljkajsdfklasjdfklasjdfklasdfjklasdjfaklsdfjaklsdjfaklsfaksdjfkasdasdklfalskdfjalskdjfalskdjflaksdjflaskjdflaksjdflaksjdfljaskldfjjdkfaklsdfjlak@company.com1
buy@tacos.com
@ -284,11 +457,18 @@ windows
windows 2000
windows
";
/// Used for the depends on chain tests
private const string justA = "A";
private const string justB = "B";
private const string justC = "C";
private static string testFilePath = string.Empty;
private static string testRulesPath = string.Empty;
private static string appliesToTestRulePath = string.Empty;
private static string doesNotApplyToTestRulePath = string.Empty;
private static string dependsOnOneWayRulePath;
private static string dependsOnChainRulePath;
private static string dependsOnTwoWayRulePath;
// Test files for timeout tests
private static readonly List<string> enumeratingTimeOutTestsFiles = new();
@ -296,6 +476,10 @@ windows
private static string heavyRulePath = string.Empty;
private ILoggerFactory factory = new NullLoggerFactory();
private static string fourWindowsOne2000Path;
private static string justAPath;
private static string justBPath;
private static string justCPath;
[ClassInitialize]
public static void ClassInit(TestContext context)
@ -308,13 +492,33 @@ windows
Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "AppliesToTestRules.json");
doesNotApplyToTestRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput),
"DoesNotApplyToTestRules.json");
dependsOnOneWayRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput),
"DependsOnOneWay.json");
dependsOnChainRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput),
"DependsOnChain.json");
dependsOnTwoWayRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput),
"DependsOnTwoWay.json");
fourWindowsOne2000Path =
Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "FourWindowsOne2000.cs");
justAPath =
Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "justA.cs");
justBPath =
Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "justB.cs");
justCPath =
Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "justC.cs");
File.WriteAllText(heavyRulePath, heavyRule);
File.WriteAllText(testFilePath, fourWindowsOneLinux);
File.WriteAllText(testRulesPath, findWindows);
File.WriteAllText(appliesToTestRulePath, findWindowsWithAppliesTo);
File.WriteAllText(doesNotApplyToTestRulePath, findWindowsWithDoesNotApplyTo);
File.WriteAllText(dependsOnOneWayRulePath, dependsOnOneWay);
File.WriteAllText(dependsOnChainRulePath, dependsOnChain);
File.WriteAllText(dependsOnTwoWayRulePath, dependsOnTwoWay);
File.WriteAllText(justAPath, justA);
File.WriteAllText(justBPath, justB);
File.WriteAllText(justCPath, justC);
File.WriteAllText(fourWindowsOne2000Path, threeWindowsOneWindows2000);
for (var i = 0; i < numTimeOutFiles; i++)
{
var newPath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), $"TestFile-{i}.js");
@ -349,10 +553,7 @@ windows
Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "OverrideTestRule.json");
var overridesWithoutOverrideTestRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput),
"OverrideTestRuleWithoutOverride.json");
var fourWindowsOne2000Path =
Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "FourWindowsOne2000.cs");
File.WriteAllText(fourWindowsOne2000Path, threeWindowsOneWindows2000);
File.WriteAllText(overridesTestRulePath, findWindowsWithOverride);
File.WriteAllText(overridesWithoutOverrideTestRulePath, findWindowsWithOverrideRuleWithoutOverride);
@ -1026,6 +1227,175 @@ windows
Assert.AreEqual(4, result.Metadata.TotalMatchesCount);
}
/// <summary>
/// Test that the depends_on rule parameter properly limits matches one way
/// </summary>
[TestMethod]
public void TestDependsOnOneWay()
{
AnalyzeOptions options = new()
{
SourcePath = new string[] { testFilePath, fourWindowsOne2000Path },
CustomRulesPath = dependsOnOneWayRulePath,
IgnoreDefaultRules = true
};
AnalyzeCommand command = new(options, factory);
var result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.AreEqual(2, result.Metadata.TotalMatchesCount);
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Dependee"));
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Dependant"));
}
/// <summary>
/// Test that the depends_on rule parameter properly limits matches one way
/// </summary>
[TestMethod]
public void TestDependsOnChain()
{
AnalyzeOptions options = new()
{
SourcePath = new string[] { justAPath, justBPath, justCPath },
CustomRulesPath = dependsOnChainRulePath,
IgnoreDefaultRules = true
};
AnalyzeCommand command = new(options, factory);
var result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.AreEqual(3, result.Metadata.TotalMatchesCount);
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Category.A"));
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Category.B"));
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Category.C"));
options = new()
{
SourcePath = new string[] { justBPath, justCPath },
CustomRulesPath = dependsOnChainRulePath,
IgnoreDefaultRules = true
};
command = new(options, factory);
result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.NoMatches, result.ResultCode);
Assert.AreEqual(0, result.Metadata.TotalMatchesCount);
Assert.IsFalse(result.Metadata.UniqueTags.Contains("Category.A"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("Category.B"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("Category.C"));
options = new()
{
SourcePath = new string[] { justAPath, justCPath },
CustomRulesPath = dependsOnChainRulePath,
IgnoreDefaultRules = true
};
command = new(options, factory);
result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.AreEqual(1, result.Metadata.TotalMatchesCount);
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Category.A"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("Category.B"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("Category.C"));
options = new()
{
SourcePath = new string[] { justAPath, justBPath },
CustomRulesPath = dependsOnChainRulePath,
IgnoreDefaultRules = true
};
command = new(options, factory);
result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.AreEqual(2, result.Metadata.TotalMatchesCount);
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Category.A"));
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Category.B"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("Category.C"));
}
/// <summary>
/// Test that the depends_on rule parameter properly limits matches one way
/// </summary>
[TestMethod]
public void TestDependsOnOneWayWithoutDependee()
{
AnalyzeOptions options = new()
{
SourcePath = new string[] { testFilePath },
CustomRulesPath = dependsOnOneWayRulePath,
IgnoreDefaultRules = true
};
AnalyzeCommand command = new(options, factory);
var result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.AreEqual(1, result.Metadata.TotalMatchesCount);
Assert.IsTrue(result.Metadata.UniqueTags.Contains("Dependee"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("Dependant"));
}
/// <summary>
/// Test that the depends_on rule parameter properly limits matches two ways
/// </summary>
[TestMethod]
public void TestDependsOnTwoWay()
{
AnalyzeOptions options = new()
{
SourcePath = new string[] { testFilePath, fourWindowsOne2000Path },
CustomRulesPath = dependsOnTwoWayRulePath,
IgnoreDefaultRules = true
};
AnalyzeCommand command = new(options, factory);
var result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.Success, result.ResultCode);
Assert.AreEqual(2, result.Metadata.TotalMatchesCount);
Assert.IsTrue(result.Metadata.UniqueTags.Contains("RuleOne"));
Assert.IsTrue(result.Metadata.UniqueTags.Contains("RuleTwo"));
}
/// <summary>
/// Test that the depends_on rule parameter properly limits matches two ways
/// </summary>
[TestMethod]
public void TestDependsOnTwoWayWithoutDependee()
{
AnalyzeOptions options = new()
{
SourcePath = new string[] { testFilePath },
CustomRulesPath = dependsOnTwoWayRulePath,
IgnoreDefaultRules = true
};
AnalyzeCommand command = new(options, factory);
var result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.NoMatches, result.ResultCode);
Assert.AreEqual(0, result.Metadata.TotalMatchesCount);
Assert.IsFalse(result.Metadata.UniqueTags.Contains("RuleOne"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("RuleTwo"));
}
[TestMethod]
public void TestDependsOnTwoWayWithoutDependant()
{
AnalyzeOptions options = new()
{
SourcePath = new string[] { fourWindowsOne2000Path },
CustomRulesPath = dependsOnTwoWayRulePath,
IgnoreDefaultRules = true
};
AnalyzeCommand command = new(options, factory);
var result = command.GetResult();
Assert.AreEqual(AnalyzeResult.ExitCode.NoMatches, result.ResultCode);
Assert.AreEqual(0, result.Metadata.TotalMatchesCount);
Assert.IsFalse(result.Metadata.UniqueTags.Contains("RuleOne"));
Assert.IsFalse(result.Metadata.UniqueTags.Contains("RuleTwo"));
}
/// <summary>
/// Test that the does_not_apply_to parameter excludes the specified types
/// </summary>

Просмотреть файл

@ -138,6 +138,101 @@ public class TestVerifyRulesCmd
}
]";
// This rule depends on another tag but that tag isn't in the ruleset
private readonly string _dependsOnTagMissingRule = @"[
{
""name"": ""Platform: Microsoft Windows"",
""id"": ""AI_TEST_WINDOWS_MUST_NOT_MATCH"",
""description"": ""This rule checks for the string 'windows'"",
""tags"": [
""Test.Tags.Windows""
],
""depends_on_tags"": [""tag_not_present""],
""applies_to"": [ ""csharp""],
""severity"": ""Important"",
""patterns"": [
{
""confidence"": ""Medium"",
""modifiers"": [
""i""
],
""pattern"": ""windows"",
""type"": ""String"",
}
],
""must-not-match"" : [ ""linux""]
}
]";
// One of these rules overrides the other and depends on another tag but that tag isn't set on the rule being overridden
private readonly string _overriddenDependsOnTagMissingRule = @"[
{
""name"": ""Platform: Microsoft Windows"",
""id"": ""_overriddenDependsOnTagMissingRule"",
""description"": ""This rule checks for the string 'windows'"",
""tags"": [
""Test.Tags.Windows""
],
""applies_to"": [ ""csharp""],
""severity"": ""Important"",
""patterns"": [
{
""confidence"": ""Medium"",
""modifiers"": [
""i""
],
""pattern"": ""windows"",
""type"": ""String"",
}
],
""must-not-match"" : [ ""linux""]
},
{
""name"": ""Platform: Microsoft Windows"",
""id"": ""_overriddenDependsOnTagMissingRule_2"",
""description"": ""This rule checks for the string 'windows'"",
""tags"": [
""Test.Tags.Windows""
],
""overrides"": [""_overriddenDependsOnTagMissingRule""],
""depends_on_tags"": [""a_tag""],
""applies_to"": [ ""csharp""],
""severity"": ""Important"",
""patterns"": [
{
""confidence"": ""Medium"",
""modifiers"": [
""i""
],
""pattern"": ""windows"",
""type"": ""String"",
}
],
""must-not-match"" : [ ""linux""]
},
{
""name"": ""Platform: Microsoft Windows"",
""id"": ""_overriddenDependsOnTagMissingRule_3"",
""description"": ""This rule checks for the string 'windows'"",
""tags"": [
""a_tag""
],
""applies_to"": [ ""csharp""],
""severity"": ""Important"",
""patterns"": [
{
""confidence"": ""Medium"",
""modifiers"": [
""i""
],
""pattern"": ""windows"",
""type"": ""String"",
}
],
""must-not-match"" : [ ""linux""]
}
]";
// MustNotMatch if specified must not be matched
private readonly string _mustNotMatchRule = @"[
{
@ -385,6 +480,38 @@ public class TestVerifyRulesCmd
File.Delete(path);
}
[TestMethod]
public void OverriddenRuleMissingDependsOnTag()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
File.WriteAllText(path, _overriddenDependsOnTagMissingRule);
VerifyRulesOptions options = new()
{
CustomRulesPath = path
};
VerifyRulesCommand command = new(options, _factory);
var result = command.GetResult();
Assert.AreEqual(VerifyRulesResult.ExitCode.NotVerified, result.ResultCode);
File.Delete(path);
}
[TestMethod]
public void MissingDependsOnTag()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
File.WriteAllText(path, _dependsOnTagMissingRule);
VerifyRulesOptions options = new()
{
CustomRulesPath = path
};
VerifyRulesCommand command = new(options, _factory);
var result = command.GetResult();
Assert.AreEqual(VerifyRulesResult.ExitCode.NotVerified, result.ResultCode);
File.Delete(path);
}
[TestMethod]
public void DuplicateIdCheckDisabled()
{

Просмотреть файл

@ -26,15 +26,36 @@ namespace Microsoft.ApplicationInspector.Commands;
/// </summary>
public class AnalyzeOptions
{
/// <summary>
/// Any number of source paths to scan.
/// </summary>
public IEnumerable<string> SourcePath { get; set; } = Array.Empty<string>();
/// <summary>
/// A path (file or directory) on disk to your custom rule file(s).
/// </summary>
public string? CustomRulesPath { get; set; }
/// <summary>
/// Disable the Default ApplicationInspector RuleSet.
/// </summary>
public bool IgnoreDefaultRules { get; set; }
/// <summary>
/// Which confidence values to use.
/// </summary>
public IEnumerable<Confidence> ConfidenceFilters { get; set; } = new[] { Confidence.High, Confidence.Medium };
/// <summary>
/// Which severity values to use.
/// </summary>
public IEnumerable<Severity> SeverityFilters { get; set; } = new[]
{ Severity.Critical | Severity.Important | Severity.Moderate | Severity.BestPractice | Severity.ManualReview };
/// <summary>
/// File paths which should be excluded from scanning.
/// </summary>
public IEnumerable<string> FilePathExclusions { get; set; } = Array.Empty<string>();
/// <summary>
/// If enabled, processing will be performed on one file at a time.
/// </summary>
public bool SingleThread { get; set; }
/// <summary>
@ -48,6 +69,9 @@ public class AnalyzeOptions
/// </summary>
public bool NoShowProgress { get; set; } = true;
/// <summary>
/// If enabled, only tags are collected, with no detailed match or file information.
/// </summary>
public bool TagsOnly { get; set; }
/// <summary>
@ -60,8 +84,17 @@ public class AnalyzeOptions
/// </summary>
public int ProcessingTimeOut { get; set; } = 0;
/// <summary>
/// Number of lines of Context to collect from each file for the Excerpt. Set to -1 to disable gathering context entirely.
/// </summary>
public int ContextLines { get; set; } = 3;
/// <summary>
/// Run rules against files for which the appropriate Language cannot be determined.
/// </summary>
public bool ScanUnknownTypes { get; set; }
/// <summary>
/// Don't gather metadata about the files scanned.
/// </summary>
public bool NoFileMetadata { get; set; }
/// <summary>
@ -70,7 +103,13 @@ public class AnalyzeOptions
/// </summary>
public int MaxNumMatchesPerTag { get; set; } = 0;
/// <summary>
/// A path to a custom comments.json file to modify the set of comment styles understood by Application Inspector.
/// </summary>
public string? CustomCommentsPath { get; set; }
/// <summary>
/// A path to a custom languages.json file to modify the set of languages understood by Application Inspector.
/// </summary>
public string? CustomLanguagesPath { get; set; }
/// <summary>
@ -100,7 +139,13 @@ public class AnalyzeOptions
/// </summary>
public bool SuccessErrorCodeOnNoMatches { get; set; }
/// <summary>
/// If set, when validating rules, require that every rule have a must-match self-test with at least one entry
/// </summary>
public bool RequireMustMatch { get; set; }
/// <summary>
/// If set, when validating rules, require that every rule have a must-not-match self-test with at least one entry
/// </summary>
public bool RequireMustNotMatch { get; set; }
}
@ -150,7 +195,7 @@ public class AnalyzeCommand
private readonly Severity _severity = Severity.Unspecified;
private readonly List<string> _srcfileList = new();
private readonly Languages _languages = new();
private readonly MetaDataHelper _metaDataHelper; //wrapper containing MetaData object to be assigned to result
private MetaDataHelper _metaDataHelper; //wrapper containing MetaData object to be assigned to result
private readonly RuleProcessor _rulesProcessor;
/// <summary>
@ -335,6 +380,11 @@ public class AnalyzeCommand
}
}
if (!_options.TagsOnly)
{
RemoveDependsOnNotPresent();
}
return AnalyzeResult.ExitCode.Success;
void ProcessAndAddToMetadata(FileEntry file)
@ -445,6 +495,7 @@ public class AnalyzeCommand
}
foreach (var matchRecord in results)
{
if (_options.TagsOnly)
{
_metaDataHelper.AddTagsFromMatchRecord(matchRecord);
@ -463,6 +514,7 @@ public class AnalyzeCommand
{
_metaDataHelper.AddMatchRecord(matchRecord);
}
}
}
}
@ -477,6 +529,38 @@ public class AnalyzeCommand
}
}
/// <summary>
/// Remove matches from the metadata when the DependsOnTags are not satisfied.
/// </summary>
private void RemoveDependsOnNotPresent()
{
List<MatchRecord> previousMatches = _metaDataHelper.Matches.ToList();
List<MatchRecord> nextMatches = FilterRecordsByMissingDependsOnTags(previousMatches);
// Continue iterating as long as records were removed in the last iteration, as their tags may have been depended on by another rule
while (nextMatches.Count != previousMatches.Count)
{
(nextMatches, previousMatches) = (FilterRecordsByMissingDependsOnTags(nextMatches), nextMatches);
}
_metaDataHelper = _metaDataHelper.CreateFresh();
foreach (MatchRecord matchRecord in nextMatches)
{
_metaDataHelper.AddMatchRecord(matchRecord);
}
}
/// <summary>
/// Return a new List of MatchRecords with records removed which depend on tags not present in the set of records.
/// Does not modify the original list.
/// </summary>
/// <param name="listToFilter"></param>
/// <returns></returns>
private List<MatchRecord> FilterRecordsByMissingDependsOnTags(List<MatchRecord> listToFilter)
{
HashSet<string> tags = listToFilter.SelectMany(x => x.Tags).Distinct().ToHashSet();
return listToFilter.Where(x => x.Rule?.DependsOnTags?.All(tag => tags.Contains(tag)) ?? true).ToList();
}
/// <summary>
/// Populate the records in the metadata asynchronously.
/// </summary>
@ -501,6 +585,11 @@ public class AnalyzeCommand
await ProcessAndAddToMetadata(entry, cancellationToken);
}
if (!_options.TagsOnly)
{
RemoveDependsOnNotPresent();
}
return AnalyzeResult.ExitCode.Success;
async Task ProcessAndAddToMetadata(FileEntry file, CancellationToken cancellationToken)
@ -565,6 +654,7 @@ public class AnalyzeCommand
}
foreach (var matchRecord in results)
{
if (_options.TagsOnly)
{
_metaDataHelper.AddTagsFromMatchRecord(matchRecord);
@ -583,6 +673,7 @@ public class AnalyzeCommand
{
_metaDataHelper.AddMatchRecord(matchRecord);
}
}
}
}
}

Просмотреть файл

@ -114,7 +114,7 @@ public class ExportTagsCommand
HashSet<string> tags = new();
foreach (var rule in _rules.GetAppInspectorRules())
foreach (var tag in rule.Tags ?? Array.Empty<string>())
foreach (var tag in (IList<string>?)rule.Tags ?? Array.Empty<string>())
tags.Add(tag);
exportTagsResult.TagsList = tags.ToList();

Просмотреть файл

@ -44,7 +44,7 @@ public class VerifyRulesResult : Result
public ExitCode ResultCode { get; set; }
[JsonPropertyName("ruleStatusList")]
public List<RuleStatus> RuleStatusList { get; set; }
public IList<RuleStatus> RuleStatusList { get; set; }
[JsonIgnore] public IEnumerable<RuleStatus> Unverified => RuleStatusList.Where(x => !x.Verified);
}

Просмотреть файл

@ -22,14 +22,17 @@ public class MetaDataHelper
{
public MetaDataHelper(string sourcePath)
{
SourcePath = sourcePath;
if (!sourcePath.Contains(','))
{
sourcePath = Path.GetFullPath(sourcePath); //normalize for .\ and similar
SourcePath = Path.GetFullPath(SourcePath); //normalize for .\ and similar
}
Metadata = new MetaData(sourcePath, sourcePath);
Metadata = new MetaData(SourcePath, SourcePath);
}
internal string SourcePath { get; set; }
//visible to callers i.e. AnalyzeCommand
internal ConcurrentDictionary<string, byte> PackageTypes { get; set; } = new();
internal ConcurrentDictionary<string, byte> FileExtensions { get; set; } = new();
@ -43,7 +46,7 @@ public class MetaDataHelper
private ConcurrentDictionary<string, byte> CloudTargets { get; } = new();
private ConcurrentDictionary<string, byte> OSTargets { get; } = new();
private ConcurrentDictionary<string, MetricTagCounter> TagCounters { get; } = new();
private ConcurrentDictionary<string, int> Languages { get; } = new();
private ConcurrentDictionary<string, int> Languages { get; set; } = new();
internal ConcurrentBag<MatchRecord> Matches { get; set; } = new();
internal ConcurrentBag<FileRecord> Files { get; set; } = new();
@ -62,7 +65,7 @@ public class MetaDataHelper
public void AddTagsFromMatchRecord(MatchRecord matchRecord)
{
//special handling for standard characteristics in report
foreach (var tag in matchRecord.Tags ?? Array.Empty<string>())
foreach (string tag in matchRecord.Tags ?? Array.Empty<string>())
switch (tag)
{
case "Metadata.Application.Author":
@ -107,31 +110,33 @@ public class MetaDataHelper
}
//Special handling; attempt to detect app types...review for multiple pattern rule limitation
var solutionType = DetectSolutionType(matchRecord);
string solutionType = DetectSolutionType(matchRecord);
if (!string.IsNullOrEmpty(solutionType))
{
AppTypes.TryAdd(solutionType, 0);
}
var CounterOnlyTagSet = false;
var selected = matchRecord.Tags is not null
bool CounterOnlyTagSet = false;
IEnumerable<KeyValuePair<string, MetricTagCounter>> selected = matchRecord.Tags is not null
? TagCounters.Where(x => matchRecord.Tags.Any(y => y.Contains(x.Value.Tag ?? "")))
: new Dictionary<string, MetricTagCounter>();
foreach (var select in selected)
foreach (KeyValuePair<string, MetricTagCounter> select in selected)
{
CounterOnlyTagSet = true;
select.Value.IncrementCount();
}
//omit adding if ther a counter metric tag
//omit adding if there is a counter metric tag
if (!CounterOnlyTagSet)
//update list of unique tags as we go
{
foreach (var tag in matchRecord.Tags ?? Array.Empty<string>())
foreach (string tag in matchRecord.Tags ?? Array.Empty<string>())
{
if (!UniqueTags.TryAdd(tag, 1))
{
UniqueTags[tag]++;
}
}
}
}
@ -144,7 +149,7 @@ public class MetaDataHelper
{
AddTagsFromMatchRecord(matchRecord);
var nonCounters = matchRecord.Tags?.Where(x => !TagCounters.Any(y => y.Key == x)) ?? Array.Empty<string>();
IEnumerable<string> nonCounters = matchRecord.Tags?.Where(x => !TagCounters.Any(y => y.Key == x)) ?? Array.Empty<string>();
//omit adding if it if all the tags were counters
if (nonCounters.Any())
@ -185,7 +190,7 @@ public class MetaDataHelper
Metadata.Languages = new SortedDictionary<string, int>(Languages);
foreach (var metricTagCounter in TagCounters.Values) Metadata.TagCounters?.Add(metricTagCounter);
foreach (MetricTagCounter metricTagCounter in TagCounters.Values) Metadata.TagCounters?.Add(metricTagCounter);
}
/// <summary>
@ -197,38 +202,6 @@ public class MetaDataHelper
Languages.AddOrUpdate(language, 1, (language, count) => count + 1);
}
/// <summary>
/// Initial best guess to deduce project name; if scanned metadata from project solution value is replaced later
/// </summary>
/// <param name="sourcePath"></param>
/// <returns></returns>
private string GetDefaultProjectName(string sourcePath)
{
var applicationName = string.Empty;
if (Directory.Exists(sourcePath))
{
if (sourcePath != string.Empty)
{
if (sourcePath[^1] == Path.DirectorySeparatorChar) //in case path ends with dir separator; remove
{
applicationName = sourcePath.Trim(Path.DirectorySeparatorChar);
}
if (applicationName.LastIndexOf(Path.DirectorySeparatorChar) is int idx && idx != -1)
{
applicationName = applicationName[idx..].Trim();
}
}
}
else
{
applicationName = Path.GetFileNameWithoutExtension(sourcePath);
}
return applicationName;
}
/// <summary>
/// Attempt to map application type tags or file type or language to identify
/// WebApplications, Windows Services, Client Apps, WebServices, Azure Functions etc.
@ -236,12 +209,12 @@ public class MetaDataHelper
/// <param name="match"></param>
public string DetectSolutionType(MatchRecord match)
{
var result = "";
string result = "";
if (match.Tags is not null && match.Tags.Any(s => s.Contains("Application.Type")))
{
foreach (var tag in match.Tags ?? Array.Empty<string>())
foreach (string tag in match.Tags ?? Array.Empty<string>())
{
var index = tag.IndexOf("Application.Type");
int index = tag.IndexOf("Application.Type");
if (-1 != index)
{
result = tag[(index + 17)..];
@ -323,7 +296,7 @@ public class MetaDataHelper
private static string ExtractJSONValue(string s)
{
var parts = s.Split(':');
string[] parts = s.Split(':');
if (parts.Length == 2)
{
return parts[1].Replace("\"", "").Trim();
@ -334,10 +307,10 @@ public class MetaDataHelper
private string ExtractXMLValue(string s)
{
var firstTag = s.IndexOf(">");
int firstTag = s.IndexOf(">");
if (firstTag > -1 && firstTag < s.Length - 1)
{
var endTag = s.IndexOf("</", firstTag);
int endTag = s.IndexOf("</", firstTag);
if (endTag > -1)
{
return s[(firstTag + 1)..endTag];
@ -349,7 +322,7 @@ public class MetaDataHelper
private string ExtractXMLValueMultiLine(string s)
{
var firstTag = s.IndexOf(">");
int firstTag = s.IndexOf(">");
if (firstTag > -1 && firstTag < s.Length - 1)
{
return s[(firstTag + 1)..];
@ -357,4 +330,18 @@ public class MetaDataHelper
return s;
}
/// <summary>
/// Returns a new MetaDataHelper with the same SourcePath, Files, Languages and FileExtensions
/// </summary>
/// <returns></returns>
internal MetaDataHelper CreateFresh()
{
return new MetaDataHelper(SourcePath)
{
Files = Files,
FileExtensions = FileExtensions,
Languages = Languages
};
}
}

Просмотреть файл

@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "1.7",
"version": "1.8-beta",
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/heads/v\\d+(?:\\.\\d+)?$"
@ -10,4 +10,4 @@
"enabled": true
}
}
}
}