Skip to content

Commit 44d7fd4

Browse files
gfsCopilot
andauthored
Add JSON schema validation for rules and tests (#626)
* Add JSON schema validation for rules and tests Introduces JSON schema validation for Application Inspector rules using JsonSchema.Net. Adds RuleSchemaProvider and rule-schema.json, updates RulesVerifier and RuleStatus to support schema validation, and extends RulesVerifierOptions for configuration. Includes unit tests for schema validation in SchemaValidationTests. * Update AppInspector.RulesEngine/Schema/RuleSchemaProvider.cs Co-authored-by: Copilot <[email protected]> * Update rule schema and improve schema validation tests Changed 'must-match' and 'must-not-match' in rule-schema.json to arrays of strings with updated descriptions. Enhanced SchemaValidationTests to validate all embedded default rules using RuleSetUtils and provide detailed error reporting for failed schema validations. * Cache schema validation results in rules Adds a SchemaValidationResult property to Rule and updates TypedRuleSet to validate and store schema results when loading rules from JSON. RulesVerifier now uses the cached schema validation result if available, avoiding redundant validations. Also updates constructors to pass the schema provider and simplifies JSON serialization in RuleSchemaProvider. * Remove line number and position from schema errors Eliminated the inclusion of line number and position details from schema validation error and warning messages in RulesVerifier. Also removed the corresponding properties from the RuleSchemaProvider error class, simplifying error reporting. * Remove debug code from schema validation test Eliminated commented-out and unused debug code related to JSON serialization and error output in SchemaValidationTests. This cleanup improves test readability and removes unnecessary code. * Add negative schema validation tests for rules Introduces multiple unit tests to verify that invalid rule definitions fail schema validation as expected. Tests cover invalid pattern types, confidence levels, severities, scopes, empty arrays, missing required fields, and schema validation error handling at both error and warning levels. * Update AppInspector.Tests/RuleProcessor/SchemaValidationTests.cs Co-authored-by: Copilot <[email protected]> * Update rule schema handling and add versioned schema Moved rule-schema.json to the project root and updated its $id. Added rule-schema-v1.json as a new versioned schema file. Updated project and code references to use the new rule-schema-v1.json as the embedded resource. * Update schema validation tests and remove rule-schema.json Replaces deprecated Assert.Empty with Assert.Fail in SchemaValidationTests and simplifies type references for schema validation result and error classes. Also removes the rule-schema.json file, possibly indicating a migration to a different schema management approach. * Refactor error collection to use stack instead of recursion Replaces recursive calls in CollectErrorsFromResult with an explicit stack to avoid potential stack overflow on deeply nested validation results. This change improves robustness when processing large or deeply nested schemas. --------- Co-authored-by: Copilot <[email protected]>
1 parent e33c40a commit 44d7fd4

File tree

10 files changed

+1261
-9
lines changed

10 files changed

+1261
-9
lines changed

AppInspector.RulesEngine/AppInspector.RulesEngine.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
<ItemGroup>
3333
<PackageReference Include="JsonCons.JsonPath" Version="1.1.0" />
34+
<PackageReference Include="JsonSchema.Net" Version="7.2.3" />
3435
<PackageReference Include="Microsoft.CST.OAT" Version="1.2.87" />
3536
<PackageReference Include="Microsoft.CST.RecursiveExtractor" Version="1.2.45" />
3637
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
@@ -45,6 +46,7 @@
4546
<ItemGroup>
4647
<EmbeddedResource Include="Resources\comments.json" />
4748
<EmbeddedResource Include="Resources\languages.json" />
49+
<EmbeddedResource Include="..\rule-schema-v1.json" />
4850
</ItemGroup>
4951

5052
<ItemGroup>

AppInspector.RulesEngine/Rule.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ public class Rule
4141
[JsonIgnore]
4242
public bool Disabled { get; set; }
4343

44+
/// <summary>
45+
/// Schema validation result from when the rule was loaded from JSON
46+
/// </summary>
47+
[JsonIgnore]
48+
public Schema.SchemaValidationResult? SchemaValidationResult { get; set; }
49+
4450
/// <summary>
4551
/// 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
4652
/// Does not work with `TagsOnly` option.
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.Linq;
3+
using Microsoft.ApplicationInspector.RulesEngine.Schema;
34
using Microsoft.CST.OAT;
45

56
namespace Microsoft.ApplicationInspector.RulesEngine;
@@ -8,10 +9,20 @@ public class RuleStatus
89
{
910
public string? RulesId { get; set; }
1011
public string? RulesName { get; set; }
11-
public bool Verified => !Errors.Any() && !OatIssues.Any();
12+
public bool Verified => !Errors.Any() && !OatIssues.Any() && !SchemaValidationErrors.Any();
1213
public IEnumerable<string> Errors { get; set; } = Enumerable.Empty<string>();
1314
public IEnumerable<Violation> OatIssues { get; set; } = Enumerable.Empty<Violation>();
1415
public bool HasPositiveSelfTests { get; set; }
1516
public bool HasNegativeSelfTests { get; set; }
16-
internal Rule Rule { get; set; }
17+
internal Rule? Rule { get; set; }
18+
19+
/// <summary>
20+
/// Schema validation errors for this rule
21+
/// </summary>
22+
public IEnumerable<SchemaValidationError> SchemaValidationErrors { get; set; } = Enumerable.Empty<SchemaValidationError>();
23+
24+
/// <summary>
25+
/// Whether the rule passed schema validation
26+
/// </summary>
27+
public bool PassedSchemaValidation { get; set; } = true;
1728
}

AppInspector.RulesEngine/RulesVerifier.cs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using JsonCons.JsonPath;
1111
using Microsoft.ApplicationInspector.Common;
1212
using Microsoft.ApplicationInspector.RulesEngine.OatExtensions;
13+
using Microsoft.ApplicationInspector.RulesEngine.Schema;
1314
using Microsoft.CST.OAT;
1415
using Microsoft.Extensions.Logging;
1516
using Microsoft.Extensions.Logging.Abstractions;
@@ -24,12 +25,18 @@ public class RulesVerifier
2425
private readonly Analyzer _analyzer;
2526
private readonly ILogger _logger;
2627
private readonly RulesVerifierOptions _options;
28+
private readonly RuleSchemaProvider? _schemaProvider;
2729

2830
public RulesVerifier(RulesVerifierOptions options)
2931
{
3032
_options = options;
3133
_logger = _options.LoggerFactory?.CreateLogger<RulesVerifier>() ?? NullLogger<RulesVerifier>.Instance;
3234
_analyzer = _options.Analyzer ?? new ApplicationInspectorAnalyzer(_options.LoggerFactory);
35+
36+
if (_options.EnableSchemaValidation)
37+
{
38+
_schemaProvider = _options.SchemaProvider ?? new RuleSchemaProvider(_options.CustomSchemaPath);
39+
}
3340
}
3441

3542
private ILoggerFactory? _loggerFactory => _options.LoggerFactory;
@@ -42,7 +49,7 @@ public RulesVerifier(RulesVerifierOptions options)
4249
/// <exception cref="OpException"></exception>
4350
public RulesVerifierResult Verify(string rulesPath)
4451
{
45-
RuleSet CompiledRuleset = new(_loggerFactory);
52+
RuleSet CompiledRuleset = new(_loggerFactory, _schemaProvider);
4653

4754
if (!string.IsNullOrEmpty(rulesPath))
4855
{
@@ -139,9 +146,53 @@ public IList<RuleStatus> CheckIntegrity(AbstractRuleSet ruleSet)
139146
public RuleStatus CheckIntegrity(ConvertedOatRule convertedOatRule)
140147
{
141148
List<string> errors = new();
149+
List<SchemaValidationError> schemaErrors = new();
150+
bool passedSchemaValidation = true;
142151

143152
// App Inspector checks
144153
var rule = convertedOatRule.AppInspectorRule;
154+
155+
// Schema validation step (use stored result from rule loading if available)
156+
if (_options.EnableSchemaValidation && _schemaProvider != null)
157+
{
158+
SchemaValidationResult validationResult;
159+
160+
// Use the stored schema validation result from rule loading if available
161+
if (rule.SchemaValidationResult != null)
162+
{
163+
validationResult = rule.SchemaValidationResult;
164+
}
165+
else
166+
{
167+
// Fallback to individual rule validation (inefficient)
168+
_logger.LogDebug("No stored schema validation result for rule {RuleId}, performing re-validation", rule.Id);
169+
validationResult = _schemaProvider.ValidateRule(rule);
170+
}
171+
172+
schemaErrors.AddRange(validationResult.Errors);
173+
passedSchemaValidation = validationResult.IsValid;
174+
175+
if (!validationResult.IsValid)
176+
{
177+
if (_options.SchemaValidationLevel == SchemaValidationLevel.Error)
178+
{
179+
foreach (var error in validationResult.Errors)
180+
{
181+
var errorMessage = $"Schema validation error at {error.Path}: {error.Message}";
182+
errors.Add(errorMessage);
183+
_logger.LogError("Schema validation error for rule {RuleId}: {Error}", rule.Id ?? "Unknown", errorMessage);
184+
}
185+
}
186+
else if (_options.SchemaValidationLevel == SchemaValidationLevel.Warning)
187+
{
188+
foreach (var error in validationResult.Errors)
189+
{
190+
var errorMessage = $"Schema validation warning at {error.Path}: {error.Message}";
191+
_logger.LogWarning("Schema validation warning for rule {RuleId}: {Error}", rule.Id ?? "Unknown", errorMessage);
192+
}
193+
}
194+
}
195+
}
145196
// Check for null Id
146197
if (string.IsNullOrEmpty(rule.Id))
147198
{
@@ -383,7 +434,9 @@ public RuleStatus CheckIntegrity(ConvertedOatRule convertedOatRule)
383434
Errors = errors,
384435
OatIssues = _analyzer.EnumerateRuleIssues(convertedOatRule),
385436
HasPositiveSelfTests = rule.MustMatch?.Count > 0,
386-
HasNegativeSelfTests = rule.MustNotMatch?.Count > 0
437+
HasNegativeSelfTests = rule.MustNotMatch?.Count > 0,
438+
SchemaValidationErrors = schemaErrors,
439+
PassedSchemaValidation = passedSchemaValidation
387440
};
388441
}
389442
}

AppInspector.RulesEngine/RulesVerifierOptions.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.CST.OAT;
1+
using Microsoft.ApplicationInspector.RulesEngine.Schema;
2+
using Microsoft.CST.OAT;
23
using Microsoft.Extensions.Logging;
34

45
namespace Microsoft.ApplicationInspector.RulesEngine;
@@ -38,4 +39,24 @@ public class RulesVerifierOptions
3839
public bool RequireMustNotMatch { get; set; }
3940

4041
public bool EnableNonBacktrackingRegex { get; set; }
42+
43+
/// <summary>
44+
/// Enable JSON schema validation for rules
45+
/// </summary>
46+
public bool EnableSchemaValidation { get; set; } = false;
47+
48+
/// <summary>
49+
/// Custom schema provider for validation
50+
/// </summary>
51+
public RuleSchemaProvider? SchemaProvider { get; set; }
52+
53+
/// <summary>
54+
/// How to handle schema validation failures
55+
/// </summary>
56+
public SchemaValidationLevel SchemaValidationLevel { get; set; } = SchemaValidationLevel.Warning;
57+
58+
/// <summary>
59+
/// Path to custom JSON schema file (optional)
60+
/// </summary>
61+
public string? CustomSchemaPath { get; set; }
4162
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (C) Microsoft. All rights reserved. Licensed under the MIT License.
22

33

4+
using Microsoft.ApplicationInspector.RulesEngine.Schema;
45
using Microsoft.Extensions.Logging;
56

67
namespace Microsoft.ApplicationInspector.RulesEngine;
@@ -11,11 +12,12 @@ namespace Microsoft.ApplicationInspector.RulesEngine;
1112
public class RuleSet : TypedRuleSet<Rule>
1213
{
1314
/// <summary>
14-
/// Create a ruleset using the given (optional) logger.
15+
/// Create a ruleset using the given (optional) logger and schema provider.
1516
/// </summary>
1617
/// <param name="loggerFactory"></param>
17-
public RuleSet(ILoggerFactory? loggerFactory = null)
18-
: base(loggerFactory)
18+
/// <param name="schemaProvider"></param>
19+
public RuleSet(ILoggerFactory? loggerFactory = null, RuleSchemaProvider? schemaProvider = null)
20+
: base(loggerFactory, schemaProvider)
1921
{
2022
}
2123
}

0 commit comments

Comments
 (0)