diff --git a/AppInspector.Tests/Commands/TestAnalyzeCmd.cs b/AppInspector.Tests/Commands/TestAnalyzeCmd.cs index 4e52c030..d0b556a3 100644 --- a/AppInspector.Tests/Commands/TestAnalyzeCmd.cs +++ b/AppInspector.Tests/Commands/TestAnalyzeCmd.cs @@ -23,6 +23,8 @@ public class TestAnalyzeCmd { private static string testFilePath = string.Empty; private static string testRulesPath = string.Empty; + private static string appliesToTestRulePath = string.Empty; + private static string doesNotApplyToTestRulePath = string.Empty; // Test files for timeout tests private static List enumeratingTimeOutTestsFiles = new(); @@ -38,9 +40,15 @@ public static void ClassInit(TestContext context) testFilePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput),"TestFile.js"); testRulesPath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "TestRules.json"); heavyRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "HeavyRule.json"); + appliesToTestRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "AppliesToTestRules.json"); + doesNotApplyToTestRulePath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput), "DoesNotApplyToTestRules.json"); + File.WriteAllText(heavyRulePath, heavyRule); File.WriteAllText(testFilePath, fourWindowsOneLinux); File.WriteAllText(testRulesPath, findWindows); + File.WriteAllText(appliesToTestRulePath, findWindowsWithAppliesTo); + File.WriteAllText(doesNotApplyToTestRulePath, findWindowsWithDoesNotApplyTo); + for (int i = 0; i < numTimeOutFiles; i++) { string newPath = Path.Combine(TestHelpers.GetPath(TestHelpers.AppPath.testOutput),$"TestFile-{i}.js"); @@ -90,6 +98,51 @@ public static void CleanUp() } ] }]"; + + private const string findWindowsWithAppliesTo = @"[ +{ + ""name"": ""Platform: Microsoft Windows"", + ""id"": ""AI_TEST_WINDOWS"", + ""description"": ""This rule checks for the string 'windows'"", + ""tags"": [ + ""Test.Tags.Windows"" + ], + ""applies_to"": [ ""javascript"" ], + ""severity"": ""Important"", + ""patterns"": [ + { + ""confidence"": ""Medium"", + ""modifiers"": [ + ""i"" + ], + ""pattern"": ""windows"", + ""type"": ""String"", + } + ] +}]"; + + private const string findWindowsWithDoesNotApplyTo = @"[ +{ + ""name"": ""Platform: Microsoft Windows"", + ""id"": ""AI_TEST_WINDOWS"", + ""description"": ""This rule checks for the string 'windows'"", + ""tags"": [ + ""Test.Tags.Windows"" + ], + ""does_not_apply_to"": [ ""javascript"" ], + ""severity"": ""Important"", + ""patterns"": [ + { + ""confidence"": ""Medium"", + ""modifiers"": [ + ""i"" + ], + ""pattern"": ""windows"", + ""type"": ""String"", + } + ] +}]"; + // These simple test rules rules look for the string "windows" and "linux" const string findWindows = @"[ @@ -895,5 +948,42 @@ public void FileMetadata() Assert.AreEqual(0, resultWithoutMetadata.Metadata.TotalFiles); } + /// + /// Test that the applies_to parameter allows the specified types + /// + [TestMethod] + public void TestAppliesTo() + { + AnalyzeOptions options = new() + { + SourcePath = new string[1] { testFilePath }, + CustomRulesPath = appliesToTestRulePath, + IgnoreDefaultRules = true + }; + + AnalyzeCommand command = new(options, factory); + AnalyzeResult result = command.GetResult(); + Assert.AreEqual(AnalyzeResult.ExitCode.Success, result.ResultCode); + Assert.AreEqual(4, result.Metadata.TotalMatchesCount); + } + + /// + /// Test that the does_not_apply_to parameter excludes the specified types + /// + [TestMethod] + public void TestDoesNotApplyTo() + { + AnalyzeOptions options = new() + { + SourcePath = new string[1] { testFilePath }, + CustomRulesPath = doesNotApplyToTestRulePath, + IgnoreDefaultRules = true + }; + + AnalyzeCommand command = new(options, factory); + AnalyzeResult result = command.GetResult(); + Assert.AreEqual(AnalyzeResult.ExitCode.NoMatches, result.ResultCode); + Assert.AreEqual(0, result.Metadata.TotalMatchesCount); + } } } \ No newline at end of file diff --git a/AppInspector.Tests/RuleProcessor/WithinClauseTests.cs b/AppInspector.Tests/RuleProcessor/WithinClauseTests.cs index 7befa58d..5e7c590f 100644 --- a/AppInspector.Tests/RuleProcessor/WithinClauseTests.cs +++ b/AppInspector.Tests/RuleProcessor/WithinClauseTests.cs @@ -23,6 +23,13 @@ public class WithinClauseTests [DataRow("WithinClauseWithoutInvertWithFindingRange")] [DataRow("WithinClauseWithInvertWithSameLine")] [DataRow("WithinClauseWithoutInvertWithSameLine")] + [DataRow("WithinClauseWithInvertWithBeforeOnly")] + [DataRow("WithinClauseWithoutInvertWithBeforeOnly")] + [DataRow("WithinClauseWithInvertWithAfterOnly")] + [DataRow("WithinClauseWithoutInvertWithAfterOnly")] + [DataRow("WithinClauseWithInvertWithSameFile")] + [DataRow("WithinClauseWithoutInvertWithSameFile")] + [DataTestMethod] public void WithinClauseInvertTest(string testDataKey) { @@ -219,6 +226,30 @@ public void MultiLineRegexCondition() { "WithinClauseWithoutInvertWithSameLine", (sameLineData, "same-line", false, 1, new[] { 14 }) + }, + { + "WithinClauseWithInvertWithBeforeOnly", + (beforeOnlyData, "only-before", true, 0, new int[] { }) + }, + { + "WithinClauseWithoutInvertWithBeforeOnly", + (beforeOnlyData, "only-before", false, 1, new[] { 3 }) + }, + { + "WithinClauseWithInvertWithAfterOnly", + (afterOnlyData, "only-after", true, 0, new int[] { }) + }, + { + "WithinClauseWithoutInvertWithAfterOnly", + (afterOnlyData, "only-after", false, 1, new[] { 2 }) + }, + { + "WithinClauseWithInvertWithSameFile", + (sameFileData, "same-file", true, 0, new int[] { }) + }, + { + "WithinClauseWithoutInvertWithSameFile", + (sameFileData, "same-file", false, 1, new[] { 2 }) } }; @@ -377,6 +408,36 @@ int main(int argc, char **argv) #define BUFSIZER1 512 #define BUFSIZER2 ((BUFSIZER1 / 2) - 8) +int main(int argc, char **argv) +{ + char *buf1R1; + char *buf2R1; + buf1R1 = (char *)malloc(BUFSIZER1); + buf2R1 = (char *)malloc(BUFSIZER1);free(buf2R1); + + strncpy(buf2R1, argv[1], BUFSIZER1 - 1); + free(buf1R1); +}"; + const string beforeOnlyData = @"#include + free(buf2R1); + buf2R1 = (char *)malloc(BUFSIZER1); +"; + const string afterOnlyData = @"#include + buf2R1 = (char *)malloc(BUFSIZER1); + free(buf2R1);"; + + const string sameFileData = @"#include + buf1R1 = (char *)malloc(BUFSIZER1); + free(buf1R1);"; + + const string findingOnlyData = @"#include +#include +#include +#include + +#define BUFSIZER1 512 +#define BUFSIZER2 ((BUFSIZER1 / 2) - 8) + int main(int argc, char **argv) { char *buf1R1; diff --git a/RulesEngine/OatExtensions/WithinClause.cs b/RulesEngine/OatExtensions/WithinClause.cs index e8d07d6c..7271531d 100644 --- a/RulesEngine/OatExtensions/WithinClause.cs +++ b/RulesEngine/OatExtensions/WithinClause.cs @@ -13,6 +13,9 @@ public WithinClause(string? field = null) : base(Operation.Custom, field) public int After { get; set; } public int Before { get; set; } + public bool OnlyBefore { get; set; } + public bool OnlyAfter { get; set; } + public bool SameFile { get; set; } public bool FindingOnly { get; set; } public bool SameLineOnly { get; set; } public PatternScope[] Scopes { get; set; } = new PatternScope[1] { PatternScope.All }; diff --git a/RulesEngine/OatExtensions/WithinOperation.cs b/RulesEngine/OatExtensions/WithinOperation.cs index 58508cf0..e03e345a 100644 --- a/RulesEngine/OatExtensions/WithinOperation.cs +++ b/RulesEngine/OatExtensions/WithinOperation.cs @@ -93,6 +93,56 @@ public OperationResult WithinOperationDelegate(Clause c, object? state1, object? toRemove.Add((clauseNum, capture)); } } + else if (wc.SameFile) + { + var start = tc.LineStarts[0]; + var end = tc.LineEnds[^1]; + var res = ProcessLambda(tc.FullContent[start..end], capture); + if (res.Result) + { + if (res.Capture is TypedClauseCapture> boundaryList) + { + passed.AddRange(boundaryList.Result); + } + } + else + { + toRemove.Add((clauseNum, capture)); + } + } + else if (wc.OnlyBefore) + { + var start = tc.LineStarts[0]; + var end = capture.Index; + var res = ProcessLambda(tc.FullContent[start..end], capture); + if (res.Result) + { + if (res.Capture is TypedClauseCapture> boundaryList) + { + passed.AddRange(boundaryList.Result); + } + } + else + { + toRemove.Add((clauseNum, capture)); + } } + else if (wc.OnlyAfter) + { + var start = capture.Index + capture.Length; + var end = tc.LineEnds[^1]; + var res = ProcessLambda(tc.FullContent[start..end], capture); + if (res.Result) + { + if (res.Capture is TypedClauseCapture> boundaryList) + { + passed.AddRange(boundaryList.Result); + } + } + else + { + toRemove.Add((clauseNum, capture)); + } + } } tcc.Result.RemoveAll(x => toRemove.Contains(x)); } @@ -137,9 +187,9 @@ public IEnumerable WithinValidationDelegate(CST.OAT.Rule rule, Clause { if (clause is WithinClause wc) { - if (new bool[] {wc.FindingOnly, wc.SameLineOnly, wc.FindingRegion}.Count(x => x) != 1) + if (new bool[] {wc.FindingOnly, wc.SameLineOnly, wc.FindingRegion, wc.OnlyAfter, wc.OnlyBefore, wc.SameFile}.Count(x => x) != 1) { - yield return new Violation($"Exactly one of: FindingOnly, SameLineOnly or FindingRegion must be set", rule, clause); + yield return new Violation($"Exactly one of: FindingOnly, SameLineOnly, OnlyAfter, OnlyBefore, SameFile or FindingRegion must be set", rule, clause); } if (wc.FindingRegion) diff --git a/RulesEngine/Rule.cs b/RulesEngine/Rule.cs index bb59a8b5..cbaa2e3c 100644 --- a/RulesEngine/Rule.cs +++ b/RulesEngine/Rule.cs @@ -35,19 +35,22 @@ public class Rule [JsonIgnore] public bool Disabled { get; set; } - [JsonProperty(PropertyName ="name")] + [JsonProperty(PropertyName = "name")] public string Name { get; set; } = ""; - [JsonProperty(PropertyName ="id")] + [JsonProperty(PropertyName = "id")] public string Id { get; set; } = ""; - [JsonProperty(PropertyName ="description")] + [JsonProperty(PropertyName = "description")] public string? Description { get; set; } = ""; - [JsonProperty(PropertyName ="applies_to")] + [JsonProperty(PropertyName = "does_not_apply_to")] + public List? DoesNotApplyTo { get; set; } + + [JsonProperty(PropertyName = "applies_to")] public string[]? AppliesTo { get; set; } - [JsonProperty(PropertyName ="applies_to_file_regex")] + [JsonProperty(PropertyName = "applies_to_file_regex")] public string[]? FileRegexes { get => _fileRegexes; @@ -70,6 +73,7 @@ public IEnumerable CompiledFileRegexes _compiled = FileRegexes?.Select(x => new Regex(x, RegexOptions.Compiled)) ?? Array.Empty(); _updateCompiledFileRegex = false; } + return _compiled; } } @@ -77,20 +81,20 @@ public IEnumerable CompiledFileRegexes private IEnumerable _compiled = Array.Empty(); private bool _updateCompiledFileRegex = false; - [JsonProperty(PropertyName ="tags")] + [JsonProperty(PropertyName = "tags")] public string[]? Tags { get; set; } - [JsonProperty(PropertyName ="severity")] + [JsonProperty(PropertyName = "severity")] [JsonConverter(typeof(StringEnumConverter))] public Severity Severity { get; set; } = Severity.Moderate; - [JsonProperty(PropertyName ="overrides")] + [JsonProperty(PropertyName = "overrides")] public string[]? Overrides { get; set; } - [JsonProperty(PropertyName ="patterns")] + [JsonProperty(PropertyName = "patterns")] public SearchPattern[] Patterns { get; set; } = Array.Empty(); - [JsonProperty(PropertyName ="conditions")] + [JsonProperty(PropertyName = "conditions")] public SearchCondition[]? Conditions { get; set; } } } \ No newline at end of file diff --git a/RulesEngine/RuleProcessor.cs b/RulesEngine/RuleProcessor.cs index 8891601c..b965fef1 100644 --- a/RulesEngine/RuleProcessor.cs +++ b/RulesEngine/RuleProcessor.cs @@ -224,7 +224,7 @@ public IEnumerable GetRulesForFile(LanguageInfo languageInfo, { return GetRulesByLanguage(languageInfo.Name) .Union(GetRulesByFileName(fileEntry.FullPath)) - .Union(GetUniversalRules()) + .Union(GetUniversalRules().Where(x => !x.AppInspectorRule.DoesNotApplyTo?.Contains(languageInfo.Name) ?? true)) .Where(x => !x.AppInspectorRule.Tags?.Any(y => tagsToIgnore?.Contains(y) ?? false) ?? true) .Where(x => !x.AppInspectorRule.Disabled && SeverityLevel.HasFlag(x.AppInspectorRule.Severity)); } @@ -241,11 +241,7 @@ public List AnalyzeFile(FileEntry fileEntry, LanguageInfo languageI { _logger.LogDebug("Failed to analyze file {path}. {type}:{message}. ({stackTrace}), fileRecord.FileName", fileEntry.FullPath, e.GetType(), e.Message, e.StackTrace); } - if (contents is not null) - { - return AnalyzeFile(contents, fileEntry, languageInfo, tagsToIgnore, numLinesContext); - } - return new List(); + return AnalyzeFile(contents, fileEntry, languageInfo, tagsToIgnore, numLinesContext); } public async Task> AnalyzeFileAsync(FileEntry fileEntry, LanguageInfo languageInfo, CancellationToken? cancellationToken = null, IEnumerable? tagsToIgnore = null, int numLinesContext = 3) @@ -363,19 +359,19 @@ List ProcessBoundary(ClauseCapture cap) /// /// Languages to filter rules for /// List of rules - private IEnumerable GetRulesByLanguage(string input) + private IEnumerable GetRulesByLanguage(string language) { if (EnableCache) { - if (_languageRulesCache.ContainsKey(input)) - return _languageRulesCache[input]; + if (_languageRulesCache.ContainsKey(language)) + return _languageRulesCache[language]; } - IEnumerable filteredRules = _ruleset.ByLanguage(input); + IEnumerable filteredRules = _ruleset.ByLanguage(language); if (EnableCache) { - _languageRulesCache.TryAdd(input, filteredRules); + _languageRulesCache.TryAdd(language, filteredRules); } return filteredRules; diff --git a/RulesEngine/Ruleset.cs b/RulesEngine/Ruleset.cs index 4697f64a..1bd00487 100644 --- a/RulesEngine/Ruleset.cs +++ b/RulesEngine/Ruleset.cs @@ -308,6 +308,48 @@ public IEnumerable GetUniversalRules() expression.Append(clauseNumber); clauseNumber++; } + else if (condition.SearchIn.Equals("same-file", StringComparison.InvariantCultureIgnoreCase)) + { + clauses.Add(new WithinClause() + { + Data = new List() { condition.Pattern.Pattern }, + Label = clauseNumber.ToString(CultureInfo.InvariantCulture), + Invert = condition.NegateFinding, + Arguments = condition.Pattern.Modifiers?.ToList() ?? new List(), + SameFile = true + }); + expression.Append(" AND "); + expression.Append(clauseNumber); + clauseNumber++; + } + else if (condition.SearchIn.Equals("only-before", StringComparison.InvariantCultureIgnoreCase)) + { + clauses.Add(new WithinClause() + { + Data = new List() { condition.Pattern.Pattern }, + Label = clauseNumber.ToString(CultureInfo.InvariantCulture), + Invert = condition.NegateFinding, + Arguments = condition.Pattern.Modifiers?.ToList() ?? new List(), + OnlyBefore = true + }); + expression.Append(" AND "); + expression.Append(clauseNumber); + clauseNumber++; + } + else if (condition.SearchIn.Equals("only-after", StringComparison.InvariantCultureIgnoreCase)) + { + clauses.Add(new WithinClause() + { + Data = new List() { condition.Pattern.Pattern }, + Label = clauseNumber.ToString(CultureInfo.InvariantCulture), + Invert = condition.NegateFinding, + Arguments = condition.Pattern.Modifiers?.ToList() ?? new List(), + OnlyAfter = true + }); + expression.Append(" AND "); + expression.Append(clauseNumber); + clauseNumber++; + } else { _logger.LogWarning("Search condition {Condition} is not one of the accepted values and this condition will be ignored",condition.SearchIn);