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:
Родитель
ec40f38f8b
Коммит
60f166d8cf
|
@ -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)
|
||||
|
|
|
@ -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 '{1}' 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче