Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e678b8e
feat: add core suppression infrastructure
yardexx Oct 22, 2025
aa653cb
feat: add mixed configuration format support
yardexx Oct 22, 2025
fe507e6
feat: integrate suppression system with analysis engine
yardexx Oct 22, 2025
94ae14b
test: add comprehensive suppression system tests
yardexx Oct 22, 2025
40c0b66
test: update configuration tests and fix existing tests
yardexx Oct 22, 2025
ea3e58c
docs: update example configuration with mixed format rules
yardexx Oct 22, 2025
0154a37
feat: add test reorganization plan and create fixtures/helpers structure
yardexx Oct 22, 2025
5a03204
test: add comprehensive unit tests for enums
yardexx Oct 22, 2025
dafc478
test: add comprehensive unit tests for models
yardexx Oct 22, 2025
f4c0547
test: add unit tests for core components
yardexx Oct 22, 2025
ad70ada
refactor: move existing tests to new unit test structure
yardexx Oct 22, 2025
e8df96c
refactor: move PreferSecureRandom test to new structure
yardexx Oct 22, 2025
4ed0636
cleanup: remove old test files after reorganization
yardexx Oct 22, 2025
b0e4eca
style: formatting
yardexx Oct 22, 2025
feae8b1
feat!: convert configuration from kebab-case to snake_case
yardexx Oct 23, 2025
3737fb3
build: regenerate code generation files for snake_case
yardexx Oct 23, 2025
b7e85de
refactor: update suppression and workspace for snake_case
yardexx Oct 23, 2025
cfa823b
docs: update examples and documentation for snake_case
yardexx Oct 23, 2025
1b7dafd
test: update test fixtures for snake_case configuration
yardexx Oct 23, 2025
0d29751
test: update unit tests for snake_case format
yardexx Oct 23, 2025
85ff3b5
docs: update changelog for snake_case breaking change
yardexx Oct 23, 2025
fc59d3a
chore: update CHANGELOG
yardexx Oct 23, 2025
e1db288
chore: raise version
yardexx Oct 23, 2025
5e52cd5
style: formatting
yardexx Oct 23, 2025
4b8c26f
chore: update description
yardexx Oct 23, 2025
862417a
chore: update todos
yardexx Oct 23, 2025
d6f73c3
chore: README
yardexx Oct 23, 2025
bb41eb2
chore: update README
yardexx Oct 23, 2025
44bf75a
chore: update todos
yardexx Oct 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0-dev.6] - 2025-10-23

### Breaking Changes
- Configuration format converted from kebab-case to snake_case to align with Dart's `analysis_options.yaml` conventions

### Added
- Comprehensive suppression system for ignoring specific rules or lines during analysis

### Changed
- Test reorganization plan and improved test coverage

### Tests
- Added comprehensive unit tests for suppression system
- Enhanced test coverage for models and enums

## [0.1.0-dev.5] - 2025-10-09

### Added
Expand Down
45 changes: 24 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

<div align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="resources/img/shield-logo.svg">
<img alt="Dart Shield" src="resources/img/shield-logo.svg" width="150">
<img
alt="Dart Shield"
src="https://github.com/yardexx/dart_shield/blob/master/resources/img/shield-logo.svg"
width="150"
>
</picture>
<p>Dart-based security-focused code analyzer which analyzes your Dart code for potential security flaws.</p>
<a href="https://github.com/yardexx/dart_shield/actions/workflows/dart.yml"><img src="https://github.com/yardexx/dart_shield/actions/workflows/dart.yml/badge.svg" alt="Pipelines: GitHub Actions"/></a>
Expand All @@ -16,8 +19,7 @@

> 🚧 UNDER CONSTRUCTION 🚧
>
> Please note that this project is still under construction and **not yet ready for production use
**.
> Please note that this project is still under construction and not yet ready for production use.
>
> Full documentation will be available once the project is ready for production use. If you have
> any questions, feel free to open an issue.
Expand All @@ -38,12 +40,13 @@ is similar to what you might expect.
- Usage of insecure HTTP connections

# Installation

> **Note:** dart_shield is not yet available on pub.dev.

To install dart_shield, run the following command:

```bash
# Using pub.dev
dart pub global activate dart_shield

# Directly from GitHub
dart pub global activate -s git https://github.com/yardexx/dart_shield
```

Expand Down Expand Up @@ -115,20 +118,20 @@ shield:

# List of rules that dart_shield will use to analyze your code
rules:
- prefer-https-over-http
- avoid-hardcoded-secrets
- prefer_https_over_http
- avoid_hardcoded_secrets

# Some rules need more fine-tuning and are marked as experimental.
# You can enable them by setting `enable-experimental` to `true`.
enable-experimental: true
# You can enable them by setting `enable_experimental` to `true`.
enable_experimental: true

# List of experimental rules that dart_shield will use to analyze your code
# ⚠️ Experimental rules are subject to change and may not be as stable as regular rules.
# ⚠️ Using "experimental-rules" without setting "enable-experimental" to "true" will cause an error.
experimental-rules:
- avoid-hardcoded-urls
- avoid-weak-hashing
- prefer-secure-random
# ⚠️ Using "experimental_rules" without setting "enable_experimental" to "true" will cause an error.
experimental_rules:
- avoid_hardcoded_urls
- avoid_weak_hashing
- prefer_secure_random
```

# Rules
Expand All @@ -138,11 +141,11 @@ similar to how linter rules enforce code style.

## List of rules

- avoid-hardcoded-secrets: Detects hardcoded secrets, such as API keys and passwords.
- avoid-hardcoded-urls: Detects hardcoded URLs.
- prefer-https-over-http: Detects the use of insecure HTTP connections.
- avoid-weak-hashing: Detects the use of weak hashing algorithms, such as MD5 and SHA-1.
- prefer-secure-random: Detects the use of non-secure random number generators.
- avoid_hardcoded_secrets: Detects hardcoded secrets, such as API keys and passwords.
- avoid_hardcoded_urls: Detects hardcoded URLs.
- prefer_https_over_http: Detects the use of insecure HTTP connections.
- avoid_weak_hashing: Detects the use of weak hashing algorithms, such as MD5 and SHA-1.
- prefer_secure_random: Detects the use of non-secure random number generators.

# Contributing

Expand Down
2 changes: 2 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
include: package:very_good_analysis/analysis_options.yaml

analyzer:
errors:
document_ignores: ignore
exclude:
- '**.g.dart'
linter:
Expand Down
24 changes: 15 additions & 9 deletions example/shield_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,24 @@ shield:
- '**.g.dart'

# List of rules that dart_shield will use to analyze your code
# You can use both simple string format and object format with exclusions
rules:
- prefer-https-over-http
- avoid-hardcoded-secrets
- prefer_https_over_http
- avoid_hardcoded_secrets:
exclude:
- lib/config.dart
- test/**

# Some rules need more fine-tuning and are marked as experimental.
# You can enable them by setting `enable-experimental` to `true`.
enable-experimental: true
# You can enable them by setting `enable_experimental` to `true`.
enable_experimental: true

# List of experimental rules that dart_shield will use to analyze your code
# ⚠️ Experimental rules are subject to change and may not be as stable as regular rules.
# ⚠️ Using "experimental-rules" without setting "enable-experimental" to "true" will cause an error.
experimental-rules:
- avoid-hardcoded-urls
- avoid-weak-hashing
- prefer-secure-random
# ⚠️ Using "experimental_rules" without setting "enable_experimental" to "true" will cause an error.
experimental_rules:
- avoid_hardcoded_urls
- avoid_weak_hashing:
exclude:
- lib/legacy/**
- prefer_secure_random
2 changes: 1 addition & 1 deletion lib/assets/shield_secrets_dart.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copy of [shield_secrets.yaml] because Dart doesn't support native assets yet.
// This is a workaround until Dart SDK adds native asset support.
// See: https://github.com/dart-lang/sdk/issues/53562
// TODO: Remove this file and use native assets when Dart SDK supports it
// TODO(yardex): Use native assets when Dart SDK supports it

const String shieldSecretsSource = r'''
shield_patterns:
Expand Down
14 changes: 9 additions & 5 deletions lib/src/security_analyzer/configuration/lint_rule_converter.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import 'package:dart_shield/src/security_analyzer/configuration/rule_config.dart';
import 'package:dart_shield/src/security_analyzer/rules/enums/enums.dart';
import 'package:dart_shield/src/security_analyzer/rules/rule/rule.dart';
import 'package:dart_shield/src/security_analyzer/rules/rule_registry.dart';
import 'package:glob/glob.dart';
import 'package:json_annotation/json_annotation.dart';

class LintRuleConverter implements JsonConverter<LintRule, String> {
class LintRuleConverter implements JsonConverter<LintRule, dynamic> {
const LintRuleConverter();

// Todo: Implement file exclusion
@override
LintRule fromJson(String name) {
LintRule fromJson(dynamic value) {
final ruleConfig = RuleConfig.fromDynamic(value);
final excludePatterns = ruleConfig.exclude.map(Glob.new).toList();

final rule = RuleRegistry.createRule(
id: RuleId.fromYamlName(name),
excludes: [],
id: RuleId.fromYamlName(ruleConfig.name),
excludes: excludePatterns,
);

return rule;
Expand Down
52 changes: 52 additions & 0 deletions lib/src/security_analyzer/configuration/rule_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// Configuration for a single rule that supports both string and
/// object formats.
class RuleConfig {
const RuleConfig({
required this.name,
this.exclude = const [],
});

/// Creates a RuleConfig from a dynamic value that can be either a String
/// or Map.
factory RuleConfig.fromDynamic(dynamic value) {
if (value is String) {
return RuleConfig(name: value);
}

if (value is Map) {
final entries = value.entries.toList();
if (entries.length != 1) {
throw ArgumentError('Rule config map must have exactly one entry');
}

final entry = entries.first;
final name = entry.key.toString();
final configValue = entry.value;

final exclude = <String>[];
if (configValue is Map) {
final excludeList = configValue['exclude'];
if (excludeList is List) {
exclude.addAll(excludeList.map((e) => e.toString()));
}
}

return RuleConfig(name: name, exclude: exclude);
} else {
throw ArgumentError(
'Rule config must be a String or Map, got ${value.runtimeType}',
);
}
}

/// The name of the rule (e.g., 'avoid_hardcoded_secrets').
final String name;

/// List of file patterns to exclude for this rule.
final List<String> exclude;

Map<String, dynamic> toJson() => {
'name': name,
'exclude': exclude,
};
}
12 changes: 6 additions & 6 deletions lib/src/security_analyzer/configuration/shield_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'package:yaml/yaml.dart';
part 'shield_config.g.dart';

@JsonSerializable(
fieldRename: FieldRename.kebab,
fieldRename: FieldRename.snake,
converters: [LintRuleConverter(), GlobConverter()],
createToJson: false,
)
Expand Down Expand Up @@ -63,7 +63,7 @@ class ShieldConfig {
.join(', ');
throw InvalidConfigurationException(
'Found experimental rule(s) in the "rules" list: $ruleNames. '
'Move these to "experimental-rules" list.',
'Move these to "experimental_rules" list.',
);
}

Expand All @@ -74,9 +74,9 @@ class ShieldConfig {
.map((rule) => rule.id.name)
.join(', ');
throw InvalidConfigurationException(
'Found experimental rule(s) in "experimental-rules" list: $ruleNames, '
'but "enable-experimental" is set to false. '
'Set "enable-experimental" to true to use these rules.',
'Found experimental rule(s) in "experimental_rules" list: $ruleNames, '
'but "enable_experimental" is set to false. '
'Set "enable_experimental" to true to use these rules.',
);
}

Expand All @@ -89,7 +89,7 @@ class ShieldConfig {
.map((rule) => rule.id.name)
.join(', ');
throw InvalidConfigurationException(
'Found non-experimental rule(s) in "experimental-rules" list: '
'Found non-experimental rule(s) in "experimental_rules" list: '
'$ruleNames. Move these to the "rules" list.',
);
}
Expand Down
8 changes: 4 additions & 4 deletions lib/src/security_analyzer/configuration/shield_config.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 13 additions & 2 deletions lib/src/security_analyzer/rules/enums/rule_id.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@ enum RuleId {
preferSecureRandom;

static RuleId fromYamlName(String name) {
// Convert kebab-case to camelCase
final camelCaseName = name.replaceAllMapped(RegExp(r'-(\w)'), (match) {
// Convert snake_case to camelCase
final camelCaseName = name.replaceAllMapped(RegExp(r'_(\w)'), (match) {
final matchStr = match.group(1);
return matchStr != null ? matchStr.toUpperCase() : '';
});

// Use RuleId.values.byName to get the enum value
return RuleId.values.byName(camelCaseName);
}

/// Converts the enum name to underscore format for use in suppression
/// comments.
/// Example: preferHttpsOverHttp -> prefer_https_over_http
String toUnderscoreCase() {
final name = this.name;
// Convert camelCase to underscore_case
return name.replaceAllMapped(RegExp('([a-z])([A-Z])'), (match) {
return '${match.group(1)}_${match.group(2)!.toLowerCase()}';
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart';

part 'matching_pattern.g.dart';

@JsonSerializable(fieldRename: FieldRename.kebab, createToJson: false)
@JsonSerializable(fieldRename: FieldRename.snake, createToJson: false)
class MatchingPattern {
MatchingPattern({
required this.name,
Expand Down
6 changes: 3 additions & 3 deletions lib/src/security_analyzer/rules/models/shield_secrets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:yaml/yaml.dart';

part 'shield_secrets.g.dart';

@JsonSerializable(fieldRename: FieldRename.kebab, createToJson: false)
@JsonSerializable(fieldRename: FieldRename.snake, createToJson: false)
class ShieldSecrets {
ShieldSecrets({
required this.version,
Expand All @@ -17,7 +17,7 @@ class ShieldSecrets {
factory ShieldSecrets.preset() {
// Workaround: Using assets.dart instead of native asset support
// See: https://github.com/dart-lang/sdk/issues/53562
// TODO: Migrate to native assets when Dart SDK supports it
// TODO(yardex): Migrate to native assets when Dart SDK supports it
// final content = File(_defaultConfigPath).readAsStringSync();
const content = shieldSecretsSource;
final dartMap = yamlToDartMap(loadYaml(content)) as Map<String, dynamic>;
Expand All @@ -31,7 +31,7 @@ class ShieldSecrets {

// Workaround: Using assets.dart instead of native asset support
// See: https://github.com/dart-lang/sdk/issues/53562
// TODO: Migrate to native assets when Dart SDK supports it
// TODO(yardex): Migrate to native assets when Dart SDK supports it
// static const _defaultConfigPath = '../rules_list/utils/shield_secrets.yaml';
static const _yamlRootKey = 'shield_patterns';

Expand Down
2 changes: 1 addition & 1 deletion lib/src/security_analyzer/rules/rule/lint_issue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class LintIssue {
required SourceSpan location,
}) {
return LintIssue(
ruleId: rule.id.name,
ruleId: rule.id.toUnderscoreCase(),
severity: rule.severity,
message: message,
location: location,
Expand Down
10 changes: 10 additions & 0 deletions lib/src/security_analyzer/rules/rule/lint_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ abstract class LintRule {
final RuleStatus status;

Iterable<LintIssue> check(ResolvedUnitResult source) {
// Check if file is excluded for this rule
if (_isFileExcluded(source.path)) {
return [];
}

final issues = collectErrorNodes(source);
return issues
.map(
Expand All @@ -33,6 +38,11 @@ abstract class LintRule {
.toList(growable: false);
}

/// Checks if the file path matches any exclusion pattern for this rule.
bool _isFileExcluded(String filePath) {
return excludes.any((glob) => glob.matches(filePath));
}

/// Collects AST nodes that violate this rule.
///
/// This method must be implemented by concrete rule implementations.
Expand Down
Loading