diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 242cbff..7fab192 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v5 - uses: dart-lang/setup-dart@v1.6.5 with: - sdk: '3.9.0' + sdk: '3.10.0' - name: โ›“ Install Dependencies run: dart pub get @@ -28,5 +28,23 @@ jobs: - name: ๐Ÿ“Š Analyze run: dart analyze --fatal-infos --fatal-warnings . + - name: ๐Ÿงช Run Tests + run: dart test --coverage=coverage + + - name: ๐Ÿ“ˆ Generate Coverage Report + run: | + dart pub global activate coverage + dart pub global run coverage:format_coverage \ + --lcov \ + --in=coverage \ + --out=coverage/lcov.info \ + --report-on=lib + + - name: ๐Ÿ“ˆ Upload Coverage + uses: codecov/codecov-action@v4 + with: + files: coverage/lcov.info + fail_ci_if_error: false + - name: ๐Ÿ“Š Run Pana run: dart pub global activate pana && dart pub global run pana diff --git a/.gitignore b/.gitignore index c19f353..52632d5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .dart_tool/ .packages build/ +pubspec.lock # Files generated during tests .test_coverage.dart @@ -21,6 +22,9 @@ coverage/ # Project .gemini/ +GEMINI.md +ROADMAP.md +RULES_CATEGORIZATION.md # Custom code_examples/ \ No newline at end of file diff --git a/.pubignore b/.pubignore index efb994d..383be22 100644 --- a/.pubignore +++ b/.pubignore @@ -1,2 +1,15 @@ +# Documentation (hosted separately) docs/ +docs.json + +# GitHub files .github/ + +# CI/CD configuration +codecov.yml + +# Tests (not needed for package consumers) +test/ + +# Internal tooling +tool/ diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index 733e60f..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,64 +0,0 @@ -# Dart Shield - -**Project Type:** Dart/Flutter CLI Application -**Status:** Under Construction (Prototype/Dev) - -## Overview -`dart_shield` is an open-source static analysis tool designed to secure Dart codebases by detecting potential vulnerabilities before they reach production. It functions similarly to a linter but focuses specifically on security flaws. - -## Key Features -- **Hardcoded Secret Detection:** Identifies API keys, passwords, and other sensitive data. -- **Insecure Connection Detection:** Flags usage of HTTP instead of HTTPS. -- **Weak Cryptography Detection:** Warns against weak hashing algorithms (MD5, SHA-1) and insecure random number generators. -- **Configurable:** Uses `shield_options.yaml` for project-specific configuration. - -## Architecture -The project follows a standard Dart CLI structure: - -- **Entry Point:** `bin/dart_shield.dart` initializes the `ShieldCommandRunner`. -- **CLI Layer:** `lib/src/cli/` handles command parsing (`init`, `analyze`) using `package:args`. -- **Core Engine:** `lib/src/core/` contains `ShieldRunner` and `AnalyzerEngine` which orchestrate the analysis process. -- **Analyzers:** - - `CodeAnalyzer` (`lib/src/analyzers/code/`): Wraps `dart analyze` (currently WIP). - - **Rules:** defined in `lib/src/analyzers/code/rules/`. -- **Configuration:** `lib/src/configuration/` manages loading and parsing of `shield_options.yaml`. -- **Reporting:** `lib/src/reporters/` handles output formatting (Console, JSON). - -## Development - -### Prerequisites -- Dart SDK: `>=3.10.0 <4.0.0` - -### Building and Running -To run the CLI from source: -```bash -dart run bin/dart_shield.dart [command] -# Example: -dart run bin/dart_shield.dart analyze -``` - -### Testing -Run unit tests: -```bash -dart test -``` - -### Code Style -The project uses `very_good_analysis` for linting. -```bash -dart analyze -``` - -## Key Files & Directories -- `bin/dart_shield.dart`: Main entry point. -- `lib/src/cli/commands/`: Implementation of `init` and `analyze` commands. -- `lib/src/analyzers/code/rules/`: specific security rules (e.g., `avoid_hardcoded_secrets.dart`). -- `shield_options.yaml`: (User-side) Configuration file for the tool. -- `analysis_options.yaml`: (Dev-side) Linter configuration for the project itself. -## Documentation Style -- All documentation must follow [Google Developer Documentation Style Guide](https://developers.google.com/style/). -- Tone: Professional, clear, concise, and direct. -- Structure: Use the agreed-upon template for rule documentation (Description, Non-Compliant/Compliant Code, How to Fix, Why, When to Ignore, Resources). - -## Rule Documentation Template -Refer to `docs/rulebook/RULE_TEMPLATE.mdx` for the mandatory structure of rule documentation files. diff --git a/README.md b/README.md index 59958a2..30f79b0 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@

Dart-based security-focused code analyzer which analyzes your Dart code for potential security flaws.

Pipelines: GitHub Actions + Coverage Style: Very Good Analysis @@ -63,12 +64,8 @@ To initialize `dart_shield` in your project, run the following command: dart_shield init ``` -This command creates a `shield_options.yaml` file in the root of your project. This file contains -the configuration for `dart_shield`, which will be used during the analysis (similar to -`analysis_options.yaml`). - -If a shield_options.yaml file already exists in your project and you want to recreate it, use the -`-f` or `--force` flag: +This command updates your `analysis_options.yaml` file to include the `dart_shield` configuration. +If the `dart_shield` section already exists and you want to recreate it, use the `-f` or `--force` flag: ```bash dart_shield init -f @@ -87,51 +84,30 @@ dart_shield analyze . dart_shield analyze lib ``` -This command analyzes your Dart code based on the configuration in the shield_options.yaml file. -If the configuration file is not found, the command will fail. +This command analyzes your Dart code for security issues. # Configuration -The `shield_options.yaml` file contains configuration options, primarily rules, for `dart_shield`. -The configuration is similar to the `analysis_options.yaml` file, making it familiar to those who -have -used Dart analysis tools. +Configuration is done through your `analysis_options.yaml` file using the `dart_shield` key. +This approach follows Dart conventions and keeps all analysis configuration in one place. -Example of the `shield_options.yaml` file: +Example configuration in `analysis_options.yaml`: ```yaml -# This is a sample configuration file for dart_shield. -# โš ๏ธ Configuration file must be named `shield_options.yaml` and placed in the root of the project. - -# shield_options.yaml is file with structure similar to analysis_options.yaml and it defines the -# rules that dart_shield will use to analyze your code. - -# The `shield` key is required. -shield: - - # List of excluded files or directories from being analyzed - exclude: - # Exclude a file using path (path begins at the root of the project): - - 'lib/ignored.dart' - # Globs are also supported - - '**.g.dart' - - # List of rules that dart_shield will use to analyze your code - rules: - - prefer_https_over_http.dart - - 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 - - # 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 +# Enable dart_shield as an analyzer plugin +analyzer: + plugins: + - dart_shield + +# dart_shield configuration +dart_shield: + analyzers: + code: true # Enable code analysis + + # Future options: + # exclude: + # - 'lib/generated/**' + # - '**.g.dart' ``` # Rules diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index a8a2542..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,130 +0,0 @@ -# Dart Shield - Feature Roadmap & Ideas - -This document tracks high-impact features that could distinguish `dart_shield` as the premier security tool for the Dart/Flutter ecosystem. - -## ๐Ÿš€ High Impact / "Killer" Features - -### 1. Dependency Shield (Supply Chain Security) -**Concept:** Integrate with the OSV (Open Source Vulnerabilities) database to scan `pubspec.lock` for packages with known security advisories. -**Value:** "Single Pane of Glass" for security. Users don't need to run `dart pub audit` separately; `dart_shield` covers both Code and Dependencies. -**Implementation:** -* Parse `pubspec.lock`. -* Query OSV API (batch). -* Report CVEs directly in the CLI output. - -### 2. "Shield Ignore" & Baselines (Legacy Support) -**Concept:** Allow teams to adopt `dart_shield` in large, existing projects without fixing 1000 legacy issues immediately. -**Value:** Eliminates the "all or nothing" adoption barrier. -**Implementation:** -* `dart_shield baseline`: Generates a `shield_baseline.yaml` recording all current issues. -* `analyze`: Ignores issues present in the baseline, reporting only *new* violations. -* Support standard `// ignore: rule_id` comments (leveraging Analyzer's ignore mechanism). - -### 3. Intelligent Auto-Fix -**Concept:** Automatically resolve common security issues. -**Value:** Reduces friction from "detection" to "resolution". -**Implementation:** -* **HTTP -> HTTPS:** Auto-rewrite URLs. -* **Weak Random:** `Random()` -> `Random.secure()`. -* **Weak Hash:** `md5` -> `sha256` (with warning). -* **Hardcoded Secret:** Suggest refactoring to `Platform.environment['VAR']` (Code Action). - -### 4. "Taint Analysis" / Data Flow Analysis (Advanced) -**Concept:** Track untrusted data (e.g., from API/User Input) to sensitive sinks (e.g., SQL query, `Process.run`). -**Value:** Detects complex vulnerabilities like SQL Injection, Command Injection, or XSS that simple regex/AST checks miss. -**Implementation:** -* Mark sources (arguments to `main`, `HttpRequest`, `stdin`). -* Mark sinks (`execute()`, `eval()`). -* Trace variable assignments through the AST to see if tainted data reaches a sink without sanitization. - -### 5. "PII Scout" (Privacy) -**Concept:** Detect potential logging or leakage of Personally Identifiable Information (PII). -**Value:** Helps with GDPR/CCPA compliance. -**Implementation:** -* Detect variable names like `email`, `password`, `ssn`, `creditCard`. -* Flag if these variables are passed to `print()`, `log()`, or sent to external analytics services (e.g., Firebase Analytics, Sentry) without hashing. - -### 6. CI/CD Integration & Reporting -**Concept:** Native support for GitHub Actions, GitLab CI, etc. -**Value:** "Plug and Play" security pipelines. -**Implementation:** -* Output formats: SARIF (standard for GitHub Security), JUnit XML. -* GitHub Action marketplace entry. -* Annotate PRs directly (using SARIF). - -### 7. "Shield Policy" (Enterprise Control) -**Concept:** Enforce security policies across an organization. -**Value:** Standardization for large teams. -**Implementation:** -* Remote config: Load `shield_options.yaml` from a URL (e.g., internal repo). -* "Strict Mode": Disallow ignores for critical severities. - -### 8. Flutter-Specific Security -**Concept:** Rules tailored for mobile risks. -**Value:** Niche dominance in the Flutter space. -**Implementation:** -* **Manifest Analysis:** Check `AndroidManifest.xml` for dangerous permissions (`READ_SMS`, `SYSTEM_ALERT_WINDOW`). -* **Plist Analysis:** Check `Info.plist` for `NSAppTransportSecurity` (allowing arbitrary loads). -* **WebView:** Flag `useHybridComposition: true` or insecure WebView settings. -* **Local Auth:** Ensure `local_auth` is used correctly. - -### 9. "Graph Shield" (Architecture Viz) -**Concept:** Visualize the security posture. -**Value:** "Manager-friendly" reports. -**Implementation:** -* Generate an HTML/Graphviz report showing dependencies and flagged hotspots. - -### 10. "Secret Canary" / "Honeytoken" Detection -**Concept:** Integrate with Canarytokens to manage or verify found secrets. -**Value:** Proactive defense. -**Implementation:** -* Check if found secrets are known Honeytoken formats. -* Suggest replacing hardcoded secrets with Honeytokens for tests. - -### 11. "Interactive Security Training" (Edu-Tech) -**Concept:** Mini-tutorials in the terminal. -**Value:** Upskills the team. -**Implementation:** -* `dart_shield explain `: Shows "Bad Code" vs "Good Code" and explains the risk. - -### 12. "Shadow Dependencies" / "Typosquatting" Detector -**Concept:** Detect malicious packages that look like popular ones (e.g., `providr` vs `provider`). -**Value:** Protects against supply chain attacks. -**Implementation:** -* Check `pubspec.yaml` against top packages using Levenshtein distance. - -### 13. "License Compliance" (Legal Shield) -**Concept:** Check dependency licenses (GPL, AGPL, etc.). -**Value:** Enterprise requirement. -**Implementation:** -* Scan package licenses. -* Flag incompatible licenses based on project type. - -### 14. "Pre-Commit Hook" Installer -**Concept:** Frictionless setup to block secrets before commit. -**Value:** Prevention > Cure. -**Implementation:** -* `dart_shield install-hook`: Installs a Git pre-commit hook. - -### 15. "Cloud Configuration Scanner" (IaC) -**Concept:** Scan Dockerfile and docker-compose.yaml. -**Value:** Full stack coverage for Dart backends. -**Implementation:** -* Flag `USER root`. -* Flag secrets in `ENV` vars. - -### 16. "Binary Inspector" (Reverse Engineering) -**Concept:** Analyze the compiled output (`.apk`, `.ipa`, `.exe`). -**Value:** Verifies what actually ships (e.g., "Did obfuscation work?"). -**Implementation:** -* **Strings:** Run `strings` on the binary to check if secrets/API keys are still visible in the compiled artifact (e.g., `libapp.so`). -* **Obfuscation Check:** Verify if symbols are stripped/obfuscated. -* **Permissions:** Read final merged manifest from APK. - -### 17. "Network Traffic Monitor" (DevTool Proxy) -**Concept:** A local proxy to inspect HTTP traffic from the running Dart app. -**Value:** Detects unencrypted traffic or sensitive data sent in cleartext during development. -**Implementation:** -* Spin up a local proxy (like helper for Charles/MITMProxy). -* Flag HTTP requests. -* Flag sensitive data (passwords, tokens) in URL parameters or bodies. \ No newline at end of file diff --git a/RULES_CATEGORIZATION.md b/RULES_CATEGORIZATION.md deleted file mode 100644 index 28d27e5..0000000 --- a/RULES_CATEGORIZATION.md +++ /dev/null @@ -1,73 +0,0 @@ -# Dart Shield Rule Categorization Concept - -This document outlines a proposed categorization for `dart_shield`'s security rules, aligning them with the OWASP Mobile Top 10 (2024) standard. This approach aims to provide a more structured and universally recognized classification for our rules, benefiting both codebase organization and user documentation. - -## Goals - -* **Standardization**: Align with an industry-recognized security standard (OWASP Mobile Top 10) to enhance clarity and relevance. -* **Organization**: Improve code structure by grouping related rules logically. -* **Documentation**: Provide a clearer, more navigable rulebook for users. - -## Proposed Categories and Rule Mapping - -Based on existing rules and common security concerns for Dart/Flutter applications, the following categories are proposed: - ---- - -### 1. Cryptography (OWASP Mobile Top 10: M10 - Insufficient Cryptography) - -This category focuses on rules designed to identify and flag insecure or weak cryptographic practices within the codebase. - -**Existing Rules:** -* `avoid_weak_hashing.dart` (Detects use of weak hashing algorithms like MD5, SHA-1) -* `prefer_secure_random.dart` (Flags non-cryptographically secure random number generators) - -**Proposed Folder Location:** -`lib/src/analyzers/code/rules/cryptography/` - ---- - -### 2. Network (OWASP Mobile Top 10: M5 - Insecure Communication) - -This category covers rules related to the security of data in transit and communication protocols, ensuring secure channels and endpoints. - -**Existing Rules:** -* `prefer_https_over_http.dart` (Warns against using insecure HTTP where HTTPS is preferred) -* `avoid_hardcoded_urls.dart` (Encourages configurable and secure URL management) - -**Proposed Folder Location:** -`lib/src/analyzers/code/rules/network/` - ---- - -### 3. Secrets (OWASP Mobile Top 10: M1 - Improper Credential Usage) - -This category addresses the detection of sensitive information, such as API keys, tokens, and credentials, being hardcoded directly into the application. - -**Existing Rules:** -* `avoid_hardcoded_secrets.dart` (Identifies various hardcoded secret patterns) - -**Proposed Folder Location:** -`lib/src/analyzers/code/rules/secrets/` (This folder already exists and contains related helper logic, making it a natural fit for this rule.) - ---- - -## Proposed Codebase Restructuring - -To implement this categorization, the following steps would be taken in the codebase: - -1. Create new subdirectories within `lib/src/analyzers/code/rules/`: - * `cryptography/` - * `network/` -2. Move the respective rule files into their new category-specific folders. -3. Update `lib/src/analyzers/code/rules/rules.dart` to reflect the new import paths for all moved rules. - -## Future Categories - -As `dart_shield` evolves, new rules can be introduced and mapped to other relevant OWASP Mobile Top 10 categories, such as: - -* **Storage** (M9: Insecure Data Storage) -* **Injection** (M4: Insufficient Input/Output Validation) -* **WebView** (M5: Insecure Communication / M4: Insufficient Input/Output Validation, depending on specific rule) - -This structured approach will help in expanding `dart_shield`'s coverage in an organized and industry-recognized manner. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..f52cb8b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,18 @@ +coverage: + precision: 2 + round: down + range: "60...100" + + status: + project: + default: + target: 70% + threshold: 5% + patch: + default: + target: 80% + +ignore: + - "**/*.g.dart" + - "lib/src/generated/**" + - "test/**" diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 184f5bd..fd796ac 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,9 +1,9 @@ include: package:lints/recommended.yaml -plugins: - dart_shield: - path: ../. +analyzer: + plugins: + - dart_shield -# I was thinking about shield: or dart_shield: -shield: - # Some config we might want to have \ No newline at end of file +dart_shield: + analyzers: + code: true \ No newline at end of file diff --git a/example/lib/vulnerabilities.dart b/example/lib/vulnerabilities.dart index c0e566e..ba2b9eb 100644 --- a/example/lib/vulnerabilities.dart +++ b/example/lib/vulnerabilities.dart @@ -41,9 +41,7 @@ void weakHashes() { // dart_shield rule: prefer-secure-random void unsecureRandom() { - // Violation: Using `Random` instead of `Random.secure()` final random = Random.secure(); - } diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index 4c1629c..0000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,332 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d - url: "https://pub.dev" - source: hosted - version: "91.0.0" - analysis_server_plugin: - dependency: transitive - description: - name: analysis_server_plugin - sha256: "26844e7f977087567135d62532b67d5639fe206c5194c3f410ba75e1a04a2747" - url: "https://pub.dev" - source: hosted - version: "0.3.3" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 - url: "https://pub.dev" - source: hosted - version: "8.4.0" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" - url: "https://pub.dev" - source: hosted - version: "0.13.10" - anio: - dependency: transitive - description: - name: anio - sha256: "7af260b3b3f228faafcfcf9626fbb302224c5022c0e5c82de8fa9644cdf9cb3c" - url: "https://pub.dev" - source: hosted - version: "2.0.10" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_completion: - dependency: transitive - description: - name: cli_completion - sha256: "72e8ccc4545f24efa7bbdf3bff7257dc9d62b072dee77513cc54295575bc9220" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - dart_shield: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.1.0-dev.6" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b - url: "https://pub.dev" - source: hosted - version: "3.1.3" - equatable: - dependency: transitive - description: - name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - url: "https://pub.dev" - source: hosted - version: "2.0.7" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - file_system: - dependency: transitive - description: - name: file_system - sha256: "98b8edb99821ae1dfac865538c04ae071ecd2a6d1910e83ae67c50af79d0fbd7" - url: "https://pub.dev" - source: hosted - version: "2.0.9" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - lints: - dependency: "direct dev" - description: - name: lints - sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c - url: "https://pub.dev" - source: hosted - version: "1.0.1" - mason_logger: - dependency: transitive - description: - name: mason_logger - sha256: "6d5a989ff41157915cb5162ed6e41196d5e31b070d2f86e1c2edf216996a158c" - url: "https://pub.dev" - source: hosted - version: "0.3.3" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - process: - dependency: transitive - description: - name: process - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 - url: "https://pub.dev" - source: hosted - version: "5.0.5" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pub_updater: - dependency: transitive - description: - name: pub_updater - sha256: "739a0161d73a6974c0675b864fb0cf5147305f7b077b7f03a58fa7a9ab3e7e7d" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" - url: "https://pub.dev" - source: hosted - version: "1.1.4" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" - yaml_edit: - dependency: transitive - description: - name: yaml_edit - sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 - url: "https://pub.dev" - source: hosted - version: "2.2.2" -sdks: - dart: ">=3.10.0 <4.0.0" diff --git a/lib/src/analyzers/code/code_analyzer.dart b/lib/src/analyzers/code/code_analyzer.dart index c5b68ad..f6e8638 100644 --- a/lib/src/analyzers/code/code_analyzer.dart +++ b/lib/src/analyzers/code/code_analyzer.dart @@ -12,10 +12,8 @@ import 'package:dart_shield/src/domain/exceptions.dart'; import 'package:path/path.dart' as path; class CodeAnalyzer implements Analyzer { - CodeAnalyzer({ - required this.analyzedPaths, - String? rootFolder, - }) : rootFolder = rootFolder ?? Directory.current.path; + CodeAnalyzer({required this.analyzedPaths, String? rootFolder}) + : rootFolder = rootFolder ?? Directory.current.path; final List analyzedPaths; final String rootFolder; @@ -36,15 +34,16 @@ class CodeAnalyzer implements Analyzer { ProcessResult result; try { - result = await Process.run( - 'dart', - ['analyze', '--format=json', target], - runInShell: true, - ); + result = await Process.run('dart', [ + 'analyze', + '--format=json', + target, + ], runInShell: true); } on ProcessException catch (e) { throw ShieldProcessException( 'Failed to execute dart analyze.', - 'Ensure the Dart SDK is installed and accessible in your PATH.\nOriginal error: ${e.message}', + 'Ensure the Dart SDK is installed and accessible in your PATH.\n' + 'Original error: ${e.message}', ); } @@ -59,7 +58,8 @@ class CodeAnalyzer implements Analyzer { if (jsonString == null) { throw ShieldProcessException( 'dart analyze did not return valid JSON output.', - 'This usually means the analysis command crashed or encountered a fatal error.\nOutput: $output', + 'This usually means the analysis command crashed or ' + 'encountered a fatal error.\nOutput: $output', ); } diff --git a/lib/src/analyzers/code/rules/cryptography/avoid_weak_hashing.dart b/lib/src/analyzers/code/rules/cryptography/avoid_weak_hashing.dart index 4b835b8..c90a76e 100644 --- a/lib/src/analyzers/code/rules/cryptography/avoid_weak_hashing.dart +++ b/lib/src/analyzers/code/rules/cryptography/avoid_weak_hashing.dart @@ -7,10 +7,7 @@ import 'package:analyzer/error/error.dart'; class AvoidWeakHashing extends AnalysisRule { AvoidWeakHashing() - : super( - name: 'avoid_weak_hashing', - description: 'Some description', - ); + : super(name: 'avoid_weak_hashing', description: 'Some description'); static const LintCode code = LintCode( 'avoid_weak_hashing', 'Using weak hashing algorithms can lead to security vulnerabilities.', @@ -65,7 +62,7 @@ class _WeakCryptoHashingVisitor extends SimpleAstVisitor { @override void visitAssignmentExpression(AssignmentExpression node) { if (_isAssignmentWeakHash(node)) { - rule.reportAtNode(node); + rule.reportAtNode(node); } } diff --git a/lib/src/analyzers/code/rules/network/prefer_https_over_http.dart b/lib/src/analyzers/code/rules/network/prefer_https_over_http.dart index b34a9a9..15ee18d 100644 --- a/lib/src/analyzers/code/rules/network/prefer_https_over_http.dart +++ b/lib/src/analyzers/code/rules/network/prefer_https_over_http.dart @@ -36,7 +36,6 @@ class PreferHttpsOverHttp extends AnalysisRule { } class _PreferHttpsOverHttpVisitor extends SimpleAstVisitor { - _PreferHttpsOverHttpVisitor({required this.rule, required this.context}); final AnalysisRule rule; final RuleContext context; diff --git a/lib/src/analyzers/code/rules/rule_metadata.dart b/lib/src/analyzers/code/rules/rule_metadata.dart new file mode 100644 index 0000000..1abd02e --- /dev/null +++ b/lib/src/analyzers/code/rules/rule_metadata.dart @@ -0,0 +1,77 @@ +import 'package:dart_shield/src/domain/analysis_issue.dart'; + +/// Metadata for security rules including severity and documentation links. +class RuleMetadata { + const RuleMetadata({ + required this.ruleId, + required this.severity, + this.owaspCategory, + this.cweId, + this.documentationUrl, + }); + + final String ruleId; + final Severity severity; + final String? owaspCategory; + final String? cweId; + final String? documentationUrl; +} + +/// Registry of all rule metadata. +/// +/// Each rule is categorized by severity level and mapped to relevant +/// security standards (OWASP, CWE). +const Map ruleMetadataRegistry = { + 'avoid_hardcoded_secrets': RuleMetadata( + ruleId: 'avoid_hardcoded_secrets', + severity: Severity.high, + owaspCategory: 'A02:2021 Cryptographic Failures', + cweId: 'CWE-798', + documentationUrl: + 'https://dart-shield.dev/rulebook/secrets/avoid-hardcoded-secrets', + ), + 'prefer_https_over_http': RuleMetadata( + ruleId: 'prefer_https_over_http', + severity: Severity.high, + owaspCategory: 'A02:2021 Cryptographic Failures', + cweId: 'CWE-319', + documentationUrl: + 'https://dart-shield.dev/rulebook/network/prefer-https-over-http', + ), + 'avoid_weak_hashing': RuleMetadata( + ruleId: 'avoid_weak_hashing', + severity: Severity.medium, + owaspCategory: 'A02:2021 Cryptographic Failures', + cweId: 'CWE-328', + documentationUrl: + 'https://dart-shield.dev/rulebook/cryptography/avoid-weak-hashing', + ), + 'prefer_secure_random': RuleMetadata( + ruleId: 'prefer_secure_random', + severity: Severity.medium, + owaspCategory: 'A02:2021 Cryptographic Failures', + cweId: 'CWE-330', + documentationUrl: + 'https://dart-shield.dev/rulebook/cryptography/prefer-secure-random', + ), + 'avoid_hardcoded_urls': RuleMetadata( + ruleId: 'avoid_hardcoded_urls', + severity: Severity.low, + owaspCategory: 'A05:2021 Security Misconfiguration', + cweId: 'CWE-547', + documentationUrl: + 'https://dart-shield.dev/rulebook/network/avoid-hardcoded-urls', + ), +}; + +/// Gets the metadata for a rule by its ID. +/// +/// Returns null if the rule ID is not found in the registry. +RuleMetadata? getRuleMetadata(String ruleId) => ruleMetadataRegistry[ruleId]; + +/// Gets the severity for a rule by its ID. +/// +/// Returns [Severity.info] if the rule ID is not found in the registry. +Severity getSeverityForRule(String ruleId) { + return ruleMetadataRegistry[ruleId]?.severity ?? Severity.info; +} diff --git a/lib/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets.dart b/lib/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets.dart index 8300f0c..8f88b83 100644 --- a/lib/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets.dart +++ b/lib/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets.dart @@ -9,10 +9,10 @@ import 'package:dart_shield/src/analyzers/utils/shannon_entropy.dart'; class AvoidHardcodedSecrets extends AnalysisRule { AvoidHardcodedSecrets() - : super( - name: 'avoid_hardcoded_secrets', - description: 'Detects hardcoded secrets, API keys, and tokens.', - ); + : super( + name: 'avoid_hardcoded_secrets', + description: 'Detects hardcoded secrets, API keys, and tokens.', + ); static const LintCode code = LintCode( 'avoid_hardcoded_secrets', @@ -56,10 +56,10 @@ class _Visitor extends SimpleAstVisitor { // If the rule has keywords, at least one must exist in value OR context. if (secretRule.keywords.isNotEmpty) { final valueLower = value.toLowerCase(); - final hasKeywordInValue = - secretRule.keywords.any(valueLower.contains); - final hasKeywordInContext = - secretRule.keywords.any(contextString.contains); + final hasKeywordInValue = secretRule.keywords.any(valueLower.contains); + final hasKeywordInContext = secretRule.keywords.any( + contextString.contains, + ); if (!hasKeywordInValue && !hasKeywordInContext) { continue; diff --git a/lib/src/analyzers/utils/analyzer_result.dart b/lib/src/analyzers/utils/analyzer_result.dart index e146b05..ea515e8 100644 --- a/lib/src/analyzers/utils/analyzer_result.dart +++ b/lib/src/analyzers/utils/analyzer_result.dart @@ -1,9 +1,6 @@ /// Root object representing the result of `dart analyze --format=json` class AnalyzeResult { - AnalyzeResult({ - required this.version, - required this.diagnostics, - }); + AnalyzeResult({required this.version, required this.diagnostics}); factory AnalyzeResult.fromJson(Map json) { return AnalyzeResult( @@ -53,10 +50,7 @@ class Diagnostic { } class Location { - Location({ - required this.file, - required this.range, - }); + Location({required this.file, required this.range}); factory Location.fromJson(Map json) { return Location( @@ -70,10 +64,7 @@ class Location { } class SourceRange { - SourceRange({ - required this.start, - required this.end, - }); + SourceRange({required this.start, required this.end}); factory SourceRange.fromJson(Map json) { return SourceRange( diff --git a/lib/src/analyzers/utils/dto_mapper.dart b/lib/src/analyzers/utils/dto_mapper.dart index 8b2697a..739ef01 100644 --- a/lib/src/analyzers/utils/dto_mapper.dart +++ b/lib/src/analyzers/utils/dto_mapper.dart @@ -1,3 +1,4 @@ +import 'package:dart_shield/src/analyzers/code/rules/rule_metadata.dart'; import 'package:dart_shield/src/analyzers/utils/analyzer_result.dart'; import 'package:dart_shield/src/domain/analysis_issue.dart'; import 'package:dart_shield/src/domain/issue_context.dart'; @@ -11,7 +12,7 @@ extension DtoDiagnosticMapper on Diagnostic { return AnalysisIssue( ruleId: code, - severity: _mapSeverity(severity), + severity: _mapSeverity(code), message: problemMessage, context: FileContext( filePath: location!.file, @@ -21,7 +22,18 @@ extension DtoDiagnosticMapper on Diagnostic { ); } - Severity _mapSeverity(String severity) { + /// Maps the rule ID to its severity level from the rule metadata registry. + /// + /// Falls back to severity based on the diagnostic severity string if the + /// rule is not found in the registry. + Severity _mapSeverity(String ruleId) { + // First, try to get severity from the rule metadata registry + final metadata = getRuleMetadata(ruleId); + if (metadata != null) { + return metadata.severity; + } + + // Fallback to diagnostic severity if rule not found switch (severity.toUpperCase()) { case 'ERROR': return Severity.high; diff --git a/lib/src/cli/commands/analyze_command.dart b/lib/src/cli/commands/analyze_command.dart index 36f713a..b20f631 100644 --- a/lib/src/cli/commands/analyze_command.dart +++ b/lib/src/cli/commands/analyze_command.dart @@ -13,6 +13,13 @@ class AnalyzeCommand extends ShieldCommand { defaultsTo: 'console', help: 'Select the output format.', ) + ..addOption( + 'min-severity', + abbr: 's', + allowed: ['info', 'low', 'medium', 'high'], + defaultsTo: 'info', + help: 'Minimum severity level to report.', + ) ..addMultiOption( 'only', allowed: AnalyzerFactory.availableIds, @@ -34,11 +41,13 @@ class AnalyzeCommand extends ShieldCommand { @override Future run() async { + final severityStr = argResults['min-severity'] as String? ?? 'info'; final config = ShieldRunConfig( paths: argResults.rest.isEmpty ? ['.'] : argResults.rest, only: argResults['only'] as List? ?? [], exclude: argResults['exclude'] as List? ?? [], reporterMode: argResults['reporter'] as String? ?? 'console', + minSeverity: ShieldRunConfig.parseSeverity(severityStr), ); final runner = ShieldRunner(logger: logger); diff --git a/lib/src/configuration/config_manager.dart b/lib/src/configuration/config_manager.dart index ad6441b..73b726f 100644 --- a/lib/src/configuration/config_manager.dart +++ b/lib/src/configuration/config_manager.dart @@ -78,9 +78,6 @@ dart_shield: '''; static const Map _defaultShieldConfig = { - 'analyzers': { - 'code': true, - 'deps': true, - }, + 'analyzers': {'code': true, 'deps': true}, }; } diff --git a/lib/src/configuration/shield_config.dart b/lib/src/configuration/shield_config.dart index 7441bb2..1d2e09e 100644 --- a/lib/src/configuration/shield_config.dart +++ b/lib/src/configuration/shield_config.dart @@ -10,11 +10,10 @@ part 'shield_config.g.dart'; anyMap: true, checked: true, disallowUnrecognizedKeys: true, + createToJson: false, ) class ShieldConfig { - const ShieldConfig({ - this.analyzers = const ShieldAnalyzersConfig(), - }); + const ShieldConfig({this.analyzers = const ShieldAnalyzersConfig()}); factory ShieldConfig.fromJson(Map map) => _$ShieldConfigFromJson(map); @@ -29,17 +28,13 @@ class ShieldConfig { if (content.trim().isEmpty) return const ShieldConfig(); try { - return checkedYamlDecode( - content, - (m) { - if (m != null && m['dart_shield'] is Map) { - return ShieldConfig.fromJson(m['dart_shield'] as Map); - } + return checkedYamlDecode(content, (m) { + if (m != null && m['dart_shield'] is Map) { + return ShieldConfig.fromJson(m['dart_shield'] as Map); + } - return const ShieldConfig(); - }, - sourceUrl: file.uri, - ); + return const ShieldConfig(); + }, sourceUrl: file.uri); } on ParsedYamlException catch (e) { throw ConfigException( 'Failed to parse analysis_options.yaml', @@ -49,12 +44,9 @@ class ShieldConfig { } } -@JsonSerializable(anyMap: true, checked: true) +@JsonSerializable(anyMap: true, checked: true, createToJson: false) class ShieldAnalyzersConfig { - const ShieldAnalyzersConfig({ - this.code = true, - this.deps = true, - }); + const ShieldAnalyzersConfig({this.code = true, this.deps = true}); factory ShieldAnalyzersConfig.fromJson(Map map) => _$ShieldAnalyzersConfigFromJson(map); diff --git a/lib/src/configuration/shield_config.g.dart b/lib/src/configuration/shield_config.g.dart index a1bd862..9bcb68a 100644 --- a/lib/src/configuration/shield_config.g.dart +++ b/lib/src/configuration/shield_config.g.dart @@ -20,9 +20,6 @@ ShieldConfig _$ShieldConfigFromJson(Map json) => return val; }); -Map _$ShieldConfigToJson(ShieldConfig instance) => - {'analyzers': instance.analyzers}; - ShieldAnalyzersConfig _$ShieldAnalyzersConfigFromJson(Map json) => $checkedCreate('ShieldAnalyzersConfig', json, ($checkedConvert) { final val = ShieldAnalyzersConfig( @@ -31,7 +28,3 @@ ShieldAnalyzersConfig _$ShieldAnalyzersConfigFromJson(Map json) => ); return val; }); - -Map _$ShieldAnalyzersConfigToJson( - ShieldAnalyzersConfig instance, -) => {'code': instance.code, 'deps': instance.deps}; diff --git a/lib/src/core/analyzer_factory.dart b/lib/src/core/analyzer_factory.dart index 1b5d3f7..9fd7e66 100644 --- a/lib/src/core/analyzer_factory.dart +++ b/lib/src/core/analyzer_factory.dart @@ -20,9 +20,7 @@ class AnalyzerFactory { List only = const [], List exclude = const [], }) { - final configEnabled = { - 'code': config.analyzers.code, - }; + final configEnabled = {'code': config.analyzers.code}; final selected = []; @@ -39,11 +37,11 @@ class AnalyzerFactory { } static bool _shouldRun( - String id, - bool isEnabledInYaml, - List only, - List exclude, - ) { + String id, + bool isEnabledInYaml, + List only, + List exclude, + ) { if (exclude.contains(id)) return false; if (only.isNotEmpty) return only.contains(id); return isEnabledInYaml; diff --git a/lib/src/core/shield_run_config.dart b/lib/src/core/shield_run_config.dart index 23e285b..7fb6ccc 100644 --- a/lib/src/core/shield_run_config.dart +++ b/lib/src/core/shield_run_config.dart @@ -1,13 +1,32 @@ +import 'package:dart_shield/src/domain/analysis_issue.dart'; + class ShieldRunConfig { const ShieldRunConfig({ required this.paths, this.only = const [], this.exclude = const [], this.reporterMode = 'console', + this.minSeverity = Severity.info, }); final List paths; final List only; final List exclude; final String reporterMode; + + /// Minimum severity level to report. + /// Issues below this severity level will be filtered out. + final Severity minSeverity; + + /// Parses a severity string to the corresponding [Severity] enum. + /// Returns [Severity.info] if the string is not recognized. + static Severity parseSeverity(String value) { + return switch (value.toLowerCase()) { + 'high' => Severity.high, + 'medium' => Severity.medium, + 'low' => Severity.low, + 'info' => Severity.info, + _ => Severity.info, + }; + } } diff --git a/lib/src/core/shield_runner.dart b/lib/src/core/shield_runner.dart index 9f65ac7..59663b7 100644 --- a/lib/src/core/shield_runner.dart +++ b/lib/src/core/shield_runner.dart @@ -2,6 +2,7 @@ import 'package:dart_shield/src/configuration/shield_config.dart'; import 'package:dart_shield/src/core/analyzer_engine.dart'; import 'package:dart_shield/src/core/analyzer_factory.dart'; import 'package:dart_shield/src/core/shield_run_config.dart'; +import 'package:dart_shield/src/domain/analysis_issue.dart'; import 'package:dart_shield/src/domain/analyzer_result.dart'; import 'package:dart_shield/src/domain/exceptions.dart'; import 'package:dart_shield/src/reporters/console_reporter.dart'; @@ -39,25 +40,50 @@ class ShieldRunner { // 4. Execution _logger.info('๐Ÿ›ก๏ธ Running ${analyzers.length} analyzers...'); final engine = AnalyzerEngine(analyzers); - final results = await engine.runAll(); + var results = await engine.runAll(); - // 5. Reporting + // 5. Filter by minimum severity + results = _filterBySeverity(results, runConfig.minSeverity); + + // 6. Reporting final reporters = _getReporters(runConfig.reporterMode); await Future.wait(reporters.map((r) => r.report(results))); - // 6. Exit Logic + // 7. Exit Logic return _calculateExitCode(results); } on ShieldException catch (e) { _logger.err(e.toString()); if (e is ConfigException) return ExitCode.config.code; return ExitCode.software.code; - } catch (e, stack) { - _logger.err('Unexpected error: $e'); - _logger.detail('$stack'); + } on Object catch (e, stack) { + _logger + ..err('Unexpected error: $e') + ..detail('$stack'); return ExitCode.software.code; } } + /// Filters analysis results to only include issues at or above the minimum + /// severity level. + List _filterBySeverity( + List results, + Severity minSeverity, + ) { + return results.map((result) { + if (result is AnalysisSuccess) { + final filteredIssues = result.issues + .where((issue) => issue.severity.index <= minSeverity.index) + .toList(); + return AnalysisSuccess( + analyzerId: result.analyzerId, + issues: filteredIssues, + duration: result.duration, + ); + } + return result; + }).toList(); + } + List _getReporters(String mode) { return switch (mode) { 'json' => [JsonReporter()], diff --git a/lib/src/domain/exceptions.dart b/lib/src/domain/exceptions.dart index 2df69bd..1cd745f 100644 --- a/lib/src/domain/exceptions.dart +++ b/lib/src/domain/exceptions.dart @@ -1,7 +1,8 @@ /// Base class for all exceptions thrown by dart_shield. /// -/// These exceptions represent "expected" failure modes (configuration errors, environment issues) that should be reported cleanly to the user, -/// as opposed to unexpected bugs (StateError, ArgumentError) which are crashes. +/// These exceptions represent "expected" failure modes (configuration errors, +/// environment issues) that should be reported cleanly to the user, as opposed +/// to unexpected bugs (StateError, ArgumentError) which are crashes. abstract class ShieldException implements Exception { const ShieldException(this.message, [this.suggestion]); diff --git a/lib/src/reporters/json_reporter.dart b/lib/src/reporters/json_reporter.dart index 037b154..1e8f452 100644 --- a/lib/src/reporters/json_reporter.dart +++ b/lib/src/reporters/json_reporter.dart @@ -3,11 +3,14 @@ import 'dart:io'; import 'package:dart_shield/src/domain/analyzer_result.dart'; import 'package:dart_shield/src/reporters/reporter.dart'; +import 'package:mason_logger/mason_logger.dart'; class JsonReporter implements Reporter { - JsonReporter({this.outputPath = 'shield_report.json'}); + JsonReporter({this.outputPath = 'shield_report.json', Logger? logger}) + : _logger = logger ?? Logger(); final String outputPath; + final Logger _logger; @override String get id => 'json'; @@ -37,6 +40,6 @@ class JsonReporter implements Reporter { await file.writeAsString(jsonStr); // Small feedback so the user knows the file was created - print('๐Ÿ’พ JSON report generated at: ${file.absolute.path}'); + _logger.info('๐Ÿ’พ JSON report generated at: ${file.absolute.path}'); } } diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 1696fe9..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,653 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d - url: "https://pub.dev" - source: hosted - version: "91.0.0" - analysis_server_plugin: - dependency: "direct main" - description: - name: analysis_server_plugin - sha256: "26844e7f977087567135d62532b67d5639fe206c5194c3f410ba75e1a04a2747" - url: "https://pub.dev" - source: hosted - version: "0.3.3" - analyzer: - dependency: "direct main" - description: - name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 - url: "https://pub.dev" - source: hosted - version: "8.4.0" - analyzer_plugin: - dependency: "direct main" - description: - name: analyzer_plugin - sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" - url: "https://pub.dev" - source: hosted - version: "0.13.10" - anio: - dependency: transitive - description: - name: anio - sha256: "7af260b3b3f228faafcfcf9626fbb302224c5022c0e5c82de8fa9644cdf9cb3c" - url: "https://pub.dev" - source: hosted - version: "2.0.10" - args: - dependency: "direct main" - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - build: - dependency: transitive - description: - name: build - sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 - url: "https://pub.dev" - source: hosted - version: "4.0.3" - build_config: - dependency: transitive - description: - name: build_config - sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 - url: "https://pub.dev" - source: hosted - version: "4.1.1" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" - url: "https://pub.dev" - source: hosted - version: "2.10.4" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" - url: "https://pub.dev" - source: hosted - version: "8.12.1" - checked_yaml: - dependency: "direct main" - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_completion: - dependency: "direct main" - description: - name: cli_completion - sha256: "72e8ccc4545f24efa7bbdf3bff7257dc9d62b072dee77513cc54295575bc9220" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - cli_config: - dependency: transitive - description: - name: cli_config - sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.dev" - source: hosted - version: "0.2.0" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" - url: "https://pub.dev" - source: hosted - version: "4.11.0" - collection: - dependency: "direct main" - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - coverage: - dependency: transitive - description: - name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.dev" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b - url: "https://pub.dev" - source: hosted - version: "3.1.3" - equatable: - dependency: transitive - description: - name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" - url: "https://pub.dev" - source: hosted - version: "2.0.7" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: "direct main" - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - file_system: - dependency: "direct main" - description: - name: file_system - sha256: "98b8edb99821ae1dfac865538c04ae071ecd2a6d1910e83ae67c50af79d0fbd7" - url: "https://pub.dev" - source: hosted - version: "2.0.9" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - glob: - dependency: "direct main" - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - http: - dependency: "direct dev" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - json_annotation: - dependency: "direct main" - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" - json_serializable: - dependency: "direct dev" - description: - name: json_serializable - sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 - url: "https://pub.dev" - source: hosted - version: "6.11.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - mason_logger: - dependency: "direct main" - description: - name: mason_logger - sha256: "6d5a989ff41157915cb5162ed6e41196d5e31b070d2f86e1c2edf216996a158c" - url: "https://pub.dev" - source: hosted - version: "0.3.3" - matcher: - dependency: transitive - description: - name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" - url: "https://pub.dev" - source: hosted - version: "0.12.18" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: "direct main" - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" - url: "https://pub.dev" - source: hosted - version: "7.0.1" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - pool: - dependency: transitive - description: - name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" - source: hosted - version: "1.5.2" - process: - dependency: transitive - description: - name: process - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 - url: "https://pub.dev" - source: hosted - version: "5.0.5" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pub_updater: - dependency: "direct main" - description: - name: pub_updater - sha256: "739a0161d73a6974c0675b864fb0cf5147305f7b077b7f03a58fa7a9ab3e7e7d" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" - source: hosted - version: "1.1.3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - source_helper: - dependency: transitive - description: - name: source_helper - sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" - url: "https://pub.dev" - source: hosted - version: "1.3.8" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" - source: hosted - version: "0.10.13" - source_span: - dependency: "direct main" - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test: - dependency: "direct dev" - description: - name: test - sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" - url: "https://pub.dev" - source: hosted - version: "1.28.0" - test_api: - dependency: transitive - description: - name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" - url: "https://pub.dev" - source: hosted - version: "0.7.8" - test_core: - dependency: transitive - description: - name: test_core - sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 - url: "https://pub.dev" - source: hosted - version: "0.6.14" - toml: - dependency: "direct dev" - description: - name: toml - sha256: "35cd2a1351c14bd213f130f8efcbd3e0c18181bff0c8ca7a08f6822a2bede786" - url: "https://pub.dev" - source: hosted - version: "0.17.0" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - very_good_analysis: - dependency: "direct dev" - description: - name: very_good_analysis - sha256: "96245839dbcc45dfab1af5fa551603b5c7a282028a64746c19c547d21a7f1e3a" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" - url: "https://pub.dev" - source: hosted - version: "1.1.4" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" - yaml_edit: - dependency: "direct main" - description: - name: yaml_edit - sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 - url: "https://pub.dev" - source: hosted - version: "2.2.2" -sdks: - dart: ">=3.10.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 675fbb3..3d994d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,10 +33,12 @@ dependencies: yaml_edit: ^2.2.2 dev_dependencies: + analyzer_testing: ^0.1.0 build_runner: ^2.10.4 http: ^1.6.0 json_serializable: ^6.11.2 test: ^1.25.5 + test_reflective_loader: ^0.2.0 toml: ^0.17.0 very_good_analysis: ^10.0.0 diff --git a/test/integration/integration_test.dart b/test/integration/integration_test.dart deleted file mode 100644 index 6132082..0000000 --- a/test/integration/integration_test.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:io'; - -import 'package:dart_shield/src/analyzers/code/code_analyzer.dart'; -import 'package:dart_shield/src/domain/analyzer_result.dart'; -import 'package:test/test.dart'; - -void main() { - group('Integration Test', () { - late File targetFile; - late String originalContent; - - setUp(() { - targetFile = File('example/lib/vulnerabilities.dart'); - originalContent = targetFile.readAsStringSync(); - }); - - tearDown(() { - targetFile.writeAsStringSync(originalContent); - }); - - test('detects hardcoded secrets via dart analyze plugin', () async { - // Inject a secret that matches our rules. - // We construct it dynamically to avoid triggering GitHub's secret scanner - // on this test file itself. - // Pattern requires [A-Z2-7]{16}. Also high entropy (> 3.0). - // ABCDEFGHIJKLMNOP uses only [A-Z], which is valid. And has max entropy. - final prefix = 'AKIA'; - final suffix = 'ABCDEFGHIJKLMNOP'; - final secret = prefix + suffix; - - final codeWithSecret = ''' -$originalContent - -void injectedSecret() { - final key = '$secret'; -} -'''; - targetFile.writeAsStringSync(codeWithSecret); - - final analyzer = CodeAnalyzer( - analyzedPaths: ['.'], // Analyze root of example - rootFolder: 'example', - ); - - final result = await analyzer.analyze(); - - expect(result, isA()); - final success = result as AnalysisSuccess; - - // Check for our specific rule - final secretIssues = success.issues - .where((i) => i.ruleId == 'avoid_hardcoded_secrets') - .toList(); - - expect( - secretIssues, - isNotEmpty, - reason: 'Should have detected the injected AWS key', - ); - - // The generic rule might ALSO match, so we just check if ONE of them is AWS. - final awsMatch = secretIssues.any( - (i) => i.message.contains('AWS credentials'), - ); - - expect( - awsMatch, - isTrue, - reason: 'Should detect AWS specific rule. Found: ${secretIssues.map((e) => e.message)}', - ); - }); - }); -} diff --git a/test/src/analyzers/code/rules/cryptography/avoid_weak_hashing_test.dart b/test/src/analyzers/code/rules/cryptography/avoid_weak_hashing_test.dart new file mode 100644 index 0000000..60cb83e --- /dev/null +++ b/test/src/analyzers/code/rules/cryptography/avoid_weak_hashing_test.dart @@ -0,0 +1,138 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:dart_shield/src/analyzers/code/rules/cryptography/avoid_weak_hashing.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidWeakHashingTest); + }); +} + +@reflectiveTest +class AvoidWeakHashingTest extends AnalysisRuleTest { + @override + String get analysisRule => 'avoid_weak_hashing'; + + @override + void setUp() { + Registry.ruleRegistry.registerLintRule(AvoidWeakHashing()); + + // Add stub for crypto package + newPackage('crypto').addFile('lib/crypto.dart', ''' +abstract class Hash { + List convert(List data); +} +final Hash md5 = _Md5(); +final Hash sha1 = _Sha1(); +final Hash sha256 = _Sha256(); +class _Md5 implements Hash { + @override + List convert(List data) => []; +} +class _Sha1 implements Hash { + @override + List convert(List data) => []; +} +class _Sha256 implements Hash { + @override + List convert(List data) => []; +} +'''); + + super.setUp(); + } + + Future test_md5Convert_reports() async { + await assertDiagnostics( + ''' +import 'package:crypto/crypto.dart'; +void f() { + final hash = md5.convert([1, 2, 3]); +} +''', + [lint(63, 22)], + ); + } + + Future test_sha1Convert_reports() async { + await assertDiagnostics( + ''' +import 'package:crypto/crypto.dart'; +void f() { + final hash = sha1.convert([1, 2, 3]); +} +''', + [lint(63, 23)], + ); + } + + Future test_sha256Convert_noReport() async { + await assertNoDiagnostics(''' +import 'package:crypto/crypto.dart'; +void f() { + final hash = sha256.convert([1, 2, 3]); +} +'''); + } + + Future test_md5Assignment_reports() async { + await assertDiagnostics( + ''' +import 'package:crypto/crypto.dart'; +void f() { + Hash hasher; + hasher = md5; +} +''', + [lint(65, 12)], + ); + } + + Future test_sha1Assignment_reports() async { + await assertDiagnostics( + ''' +import 'package:crypto/crypto.dart'; +void f() { + Hash hasher; + hasher = sha1; +} +''', + [lint(65, 13)], + ); + } + + Future test_sha256Assignment_noReport() async { + await assertNoDiagnostics(''' +import 'package:crypto/crypto.dart'; +void f() { + Hash hasher; + hasher = sha256; +} +'''); + } + + Future test_md5InExpression_reports() async { + await assertDiagnostics( + ''' +import 'package:crypto/crypto.dart'; +void f() { + final result = md5.convert([1]).toString(); +} +''', + [lint(65, 16)], + ); + } + + Future test_unrelatedIdentifier_noReport() async { + // Identifiers named md5 or sha1 that are not from crypto package + await assertNoDiagnostics(''' +void f() { + final md5 = 'some string'; + print(md5); +} +'''); + } +} diff --git a/test/src/analyzers/code/rules/cryptography/prefer_secure_random_test.dart b/test/src/analyzers/code/rules/cryptography/prefer_secure_random_test.dart new file mode 100644 index 0000000..573c760 --- /dev/null +++ b/test/src/analyzers/code/rules/cryptography/prefer_secure_random_test.dart @@ -0,0 +1,109 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:dart_shield/src/analyzers/code/rules/cryptography/prefer_secure_random.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferSecureRandomTest); + }); +} + +@reflectiveTest +class PreferSecureRandomTest extends AnalysisRuleTest { + @override + String get analysisRule => 'prefer_secure_random'; + + @override + void setUp() { + Registry.ruleRegistry.registerLintRule(PreferSecureRandom()); + super.setUp(); + } + + Future test_randomConstructor_reports() async { + await assertDiagnostics( + ''' +import 'dart:math'; +void f() { + final rng = Random(); +} +''', + [lint(45, 8)], + ); + } + + Future test_randomWithSeed_reports() async { + await assertDiagnostics( + ''' +import 'dart:math'; +void f() { + final rng = Random(42); +} +''', + [lint(45, 10)], + ); + } + + Future test_randomInClassField_reports() async { + await assertDiagnostics( + ''' +import 'dart:math'; +class MyClass { + final rng = Random(); +} +''', + [lint(50, 8)], + ); + } + + Future test_randomInFunction_reports() async { + await assertDiagnostics( + ''' +import 'dart:math'; +int getRandomNumber() { + return Random().nextInt(100); +} +''', + [lint(53, 8)], + ); + } + + Future test_multipleRandomInstances_reportsEach() async { + await assertDiagnostics( + ''' +import 'dart:math'; +void f() { + final rng1 = Random(); + final rng2 = Random(123); +} +''', + [lint(46, 8), lint(71, 11)], + ); + } + + Future test_randomWithVariableSeed_reports() async { + await assertDiagnostics( + ''' +import 'dart:math'; +void f(int seed) { + final rng = Random(seed); +} +''', + [lint(53, 12)], + ); + } + + Future test_randomWithTimestampSeed_reports() async { + await assertDiagnostics( + ''' +import 'dart:math'; +void f() { + final rng = Random(DateTime.now().millisecondsSinceEpoch); +} +''', + [lint(45, 45)], + ); + } +} diff --git a/test/src/analyzers/code/rules/network/avoid_hardcoded_urls_test.dart b/test/src/analyzers/code/rules/network/avoid_hardcoded_urls_test.dart new file mode 100644 index 0000000..d982c10 --- /dev/null +++ b/test/src/analyzers/code/rules/network/avoid_hardcoded_urls_test.dart @@ -0,0 +1,124 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:dart_shield/src/analyzers/code/rules/network/avoid_harcoded_urls.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidHardcodedUrlsTest); + }); +} + +@reflectiveTest +class AvoidHardcodedUrlsTest extends AnalysisRuleTest { + @override + String get analysisRule => 'avoid_hardcoded_urls'; + + @override + void setUp() { + Registry.ruleRegistry.registerLintRule(AvoidHardcodedUrls()); + super.setUp(); + } + + Future test_httpsUrl_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'https://api.example.com/v1'; +} +''', + [lint(25, 28)], + ); + } + + Future test_httpUrl_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'http://api.example.com'; +} +''', + [lint(25, 24)], + ); + } + + Future test_emptyString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final url = ''; +} +'''); + } + + Future test_nonUrlString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final text = 'hello world'; +} +'''); + } + + Future test_localhostUrl_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'http://localhost:8080/api'; +} +''', + [lint(25, 27)], + ); + } + + Future test_httpsWithQueryParams_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'https://api.example.com/search?q=test'; +} +''', + [lint(25, 39)], + ); + } + + Future test_multipleUrls_reportsEach() async { + await assertDiagnostics( + ''' +void f() { + final url1 = 'https://api.example.com'; + final url2 = 'http://other.example.com'; +} +''', + [lint(26, 25), lint(68, 26)], + ); + } + + Future test_urlInClassField_reports() async { + await assertDiagnostics( + ''' +class Config { + final baseUrl = 'https://api.example.com'; +} +''', + [lint(33, 25)], + ); + } + + Future test_urlInConstant_reports() async { + await assertDiagnostics( + ''' +const apiUrl = 'https://api.example.com/v1'; +''', + [lint(15, 28)], + ); + } + + Future test_partialUrl_noReport() async { + await assertNoDiagnostics(''' +void f() { + final path = '/api/v1/users'; +} +'''); + } +} diff --git a/test/src/analyzers/code/rules/network/prefer_https_over_http_test.dart b/test/src/analyzers/code/rules/network/prefer_https_over_http_test.dart new file mode 100644 index 0000000..17e2d9c --- /dev/null +++ b/test/src/analyzers/code/rules/network/prefer_https_over_http_test.dart @@ -0,0 +1,141 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:dart_shield/src/analyzers/code/rules/network/prefer_https_over_http.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferHttpsOverHttpTest); + }); +} + +@reflectiveTest +class PreferHttpsOverHttpTest extends AnalysisRuleTest { + @override + String get analysisRule => 'prefer_https_over_http'; + + @override + void setUp() { + Registry.ruleRegistry.registerLintRule(PreferHttpsOverHttp()); + super.setUp(); + } + + Future test_httpStringLiteral_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'http://example.com'; +} +''', + [lint(25, 20)], + ); + } + + Future test_httpsStringLiteral_noReport() async { + await assertNoDiagnostics(''' +void f() { + final url = 'https://example.com'; +} +'''); + } + + Future test_httpWithPath_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'http://example.com/api/v1'; +} +''', + [lint(25, 27)], + ); + } + + Future test_emptyString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final url = ''; +} +'''); + } + + Future test_nonUrlString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final text = 'hello world'; +} +'''); + } + + Future test_httpInVariable_reports() async { + await assertDiagnostics( + ''' +void f() { + final config = 'http://api.example.com'; +} +''', + [lint(28, 24)], + ); + } + + Future test_httpLocalhost_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'http://localhost:8080'; +} +''', + [lint(25, 23)], + ); + } + + Future test_httpWithPort_reports() async { + await assertDiagnostics( + ''' +void f() { + final url = 'http://example.com:3000/api'; +} +''', + [lint(25, 29)], + ); + } + + Future test_httpInList_reports() async { + await assertDiagnostics( + ''' +void f() { + final urls = [ + 'http://example.com', + ]; +} +''', + [lint(32, 20)], + ); + } + + Future test_httpInMap_reports() async { + await assertDiagnostics( + ''' +void f() { + final config = { + 'url': 'http://example.com', + }; +} +''', + [lint(41, 20)], + ); + } + + Future test_multipleHttpUrls_reportsEach() async { + await assertDiagnostics( + ''' +void f() { + final url1 = 'http://example.com'; + final url2 = 'http://other.com'; +} +''', + [lint(26, 20), lint(63, 18)], + ); + } +} diff --git a/test/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets_test.dart b/test/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets_test.dart new file mode 100644 index 0000000..a9b731d --- /dev/null +++ b/test/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets_test.dart @@ -0,0 +1,145 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:dart_shield/src/analyzers/code/rules/secrets/avoid_hardcoded_secrets.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidHardcodedSecretsTest); + }); +} + +@reflectiveTest +class AvoidHardcodedSecretsTest extends AnalysisRuleTest { + @override + String get analysisRule => 'avoid_hardcoded_secrets'; + + @override + void setUp() { + Registry.ruleRegistry.registerLintRule(AvoidHardcodedSecrets()); + super.setUp(); + } + + Future test_awsAccessKey_reports() async { + // AWS access keys start with AKIA and are 20 chars + await assertDiagnostics( + ''' +void f() { + final key = 'AKIAIOSFODNN7EXAMPLE'; +} +''', + [lint(25, 22)], + ); + } + + Future test_githubToken_reports() async { + await assertDiagnostics( + ''' +void f() { + final token = 'ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345'; +} +''', + [lint(27, 38)], + ); + } + + Future test_shortString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final s = 'short'; +} +'''); + } + + Future test_lowEntropyString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final s = 'aaaaaaaaaaaaaaaa'; +} +'''); + } + + Future test_contextVariableName_detects() async { + // Tests that apiKey variable name triggers keyword check with Stripe + // pattern + await assertDiagnostics( + ''' +void f() { + final apiKey = 'sk_live_abcdefghijklmnop'; +} +''', + [lint(28, 26)], + ); + } + + Future test_mapKeyContext_detects() async { + await assertDiagnostics( + ''' +void f() { + final config = { + 'apiKey': 'sk_test_1234567890abcdef', + }; +} +''', + [lint(44, 26)], + ); + } + + // Note: Slack token test removed to avoid GitHub push protection + // false positives. The xoxb- pattern is still tested in integration tests + // with real project scans. + + Future test_genericApiKeyPattern_reports() async { + // Generic API key with sufficient entropy + await assertDiagnostics( + ''' +void f() { + final apiKey = 'api_key_xK9mN2pL5qR8vW3yZ1aB4cD7eF0gH'; +} +''', + [lint(28, 39)], + ); + } + + Future test_regularString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final message = 'Hello, World!'; +} +'''); + } + + Future test_emptyString_noReport() async { + await assertNoDiagnostics(''' +void f() { + final s = ''; +} +'''); + } + + Future test_namedParameterContext_detects() async { + await assertDiagnostics( + ''' +void configure({required String apiKey}) {} +void f() { + configure(apiKey: 'sk_live_abc123def456ghi789'); +} +''', + [lint(75, 28)], + ); + } + + Future test_jwtToken_reports() async { + // JWT tokens have distinctive patterns + await assertDiagnostics( + ''' +void f() { + final token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; +} +''', + [lint(27, 157)], + ); + } +} diff --git a/test/src/analyzers/code/rules/secrets/secret_rule_test.dart b/test/src/analyzers/code/rules/secrets/secret_rule_test.dart index 8db2749..df7139a 100644 --- a/test/src/analyzers/code/rules/secrets/secret_rule_test.dart +++ b/test/src/analyzers/code/rules/secrets/secret_rule_test.dart @@ -44,7 +44,7 @@ void main() { description: 'desc', pattern: RegExp('abc', caseSensitive: false, multiLine: true), keywords: ['k'], - minEntropy: 2.0, + minEntropy: 2, ); final json = rule.toJson(); diff --git a/test/src/analyzers/utils/shannon_entropy_test.dart b/test/src/analyzers/utils/shannon_entropy_test.dart index b3cca21..22b7738 100644 --- a/test/src/analyzers/utils/shannon_entropy_test.dart +++ b/test/src/analyzers/utils/shannon_entropy_test.dart @@ -34,7 +34,7 @@ void main() { test('calculates entropy for typical API key examples', () { // Low entropy string final low = ShannonEntropy.calculate('password123'); - + // High entropy string (simulated API key) final high = ShannonEntropy.calculate('7Fz92xK1qM4bJ8vR'); diff --git a/test/src/configuration/shield_config_test.dart b/test/src/configuration/shield_config_test.dart index 1e15a54..5ee47bb 100644 --- a/test/src/configuration/shield_config_test.dart +++ b/test/src/configuration/shield_config_test.dart @@ -49,7 +49,7 @@ dart_shield: analyzers: [ '''); // Broken YAML expect( - () async => await ShieldConfig.load(), + ShieldConfig.load, throwsA(isA()), ); }); @@ -60,7 +60,7 @@ dart_shield: analyzers: "invalid_string" '''); expect( - () async => await ShieldConfig.load(), + ShieldConfig.load, throwsA(isA()), ); }); diff --git a/test/tool/utils/regex_sanitizer_test.dart b/test/tool/utils/regex_sanitizer_test.dart index ca5dcb7..c4184f5 100644 --- a/test/tool/utils/regex_sanitizer_test.dart +++ b/test/tool/utils/regex_sanitizer_test.dart @@ -36,8 +36,8 @@ void main() { }); test('leaves standard regex untouched', () { - final result = RegexSanitizer.sanitize('^abc[0-9]+\$'); - expect(result.pattern, '^abc[0-9]+\$'); + final result = RegexSanitizer.sanitize(r'^abc[0-9]+$'); + expect(result.pattern, r'^abc[0-9]+$'); expect(result.caseSensitive, isTrue); expect(result.multiLine, isFalse); }); diff --git a/tool/generate_rules.dart b/tool/generate_rules.dart index b0d09c3..1e3dca4 100644 --- a/tool/generate_rules.dart +++ b/tool/generate_rules.dart @@ -70,7 +70,9 @@ Future main() async { } print( - 'Processed ${rulesList.length} rules. Valid: ${validRules.length}. Skipped: $skippedCount.'); + 'Processed ${rulesList.length} rules. ' + 'Valid: ${validRules.length}. Skipped: $skippedCount.', + ); final jsonContent = const JsonEncoder.withIndent(' ').convert(validRules); @@ -84,8 +86,8 @@ Future main() async { final dartContent = '// GENERATED CODE - DO NOT MODIFY BY HAND\n' '// Generated by tool/generate_rules.dart\n\n' - "const String fallbackRulesJson = r'''\n" + - jsonContent + + "const String fallbackRulesJson = r'''\n" + '$jsonContent' "\n''';\n"; await dartFile.writeAsString(dartContent); diff --git a/tool/utils/regex_sanitizer.dart b/tool/utils/regex_sanitizer.dart index 1500b45..65ef237 100644 --- a/tool/utils/regex_sanitizer.dart +++ b/tool/utils/regex_sanitizer.dart @@ -1,10 +1,10 @@ /// Utility to sanitize and adapt Regex patterns from other languages (Go/PCRE) /// to Dart's JavaScript-flavored RegExp engine. /// -/// Dart's [RegExp] is based on JavaScript's regex engine, which is less powerful -/// than PCRE or Go's `regexp` package. This class attempts to bridge the gap by -/// modifying patterns to be compatible, primarily focusing on flags that Dart -/// handles via constructor arguments rather than inline syntax. +/// Dart's [RegExp] is based on JavaScript's regex engine, which is less +/// powerful than PCRE or Go's `regexp` package. This class attempts to bridge +/// the gap by modifying patterns to be compatible, primarily focusing on flags +/// that Dart handles via constructor arguments rather than inline syntax. class RegexSanitizer { /// Sanitizes a raw regex string and determines the necessary Dart flags. /// @@ -17,14 +17,15 @@ class RegexSanitizer { // 1. Handle Global Case Insensitivity `(?i)` // Go/PCRE use `(?i)` at the start (or inline) to enable case insensitivity. - // Dart requires this as a constructor argument: `RegExp(p, caseSensitive: false)`. + // Dart requires this as a constructor argument: + // `RegExp(p, caseSensitive: false)`. // - // Strategy: If `(?i)` is present anywhere, we remove it and set `caseSensitive` - // to false globally. This is a safe approximation: + // Strategy: If `(?i)` is present anywhere, we remove it and set + // `caseSensitive` to false globally. This is a safe approximation: // - Start: `(?i)abc` -> `abc` (caseSensitive: false) [Exact Match] - // - Inline: `abc(?i)def` -> `abcdef` (caseSensitive: false) [Broader Match] - // (Go matches 'abcDEF', Dart matches 'ABCDEF'. This increases recall but - // might slightly increase false positives, which is acceptable for secret scanning). + // - Inline: `abc(?i)def` -> `abcdef` (caseSensitive: false) [Broader] + // (Go matches 'abcDEF', Dart matches 'ABCDEF'. This increases recall + // but might slightly increase false positives, acceptable for scans). if (pattern.contains('(?i)')) { caseSensitive = false; pattern = pattern.replaceAll('(?i)', ''); @@ -36,16 +37,16 @@ class RegexSanitizer { // // Strategy: Convert `(?i:...)` to a standard non-capturing group `(?:...)` // and enable global case insensitivity. - // Impact: Similar to inline flags, this broadens the match to be case-insensitive - // for the *entire* string. + // Impact: Similar to inline flags, this broadens the match to be + // case-insensitive for the *entire* string. if (pattern.contains('(?i:')) { caseSensitive = false; pattern = pattern.replaceAll('(?i:', '(?:'); } // 3. Handle Multi-line flag `(?m)` - // Go/PCRE use `(?m)` to make `^` and `$` match start/end of lines, not just string. - // Dart handles this via `multiLine: true`. + // Go/PCRE use `(?m)` to make `^` and `$` match start/end of lines, + // not just string. Dart handles this via `multiLine: true`. if (pattern.contains('(?m)')) { multiLine = true; pattern = pattern.replaceAll('(?m)', ''); @@ -59,22 +60,20 @@ class RegexSanitizer { } // 5. Handle "Single-line" / Dot-all flag `(?s)` - // Go/PCRE `(?s)` makes `.` match newlines. - // Dart `dotAll: true` handles this. + // Go/PCRE `(?s)` makes `.` match newlines. Dart `dotAll: true` handles this. if (pattern.contains('(?s)')) { - // Note: Dart's `dotAll` is available. - // However, we need to pass this back to the caller. - // For now, we'll assume standard DotAll behavior isn't critical or handle it later. - // Actually, let's strip it to allow compilation. + // Note: Dart's `dotAll` is available. However, we need to pass this back + // to the caller. For now, we'll assume standard DotAll behavior isn't + // critical or handle it later. Let's strip it to allow compilation. pattern = pattern.replaceAll('(?s)', ''); - // TODO: Return dotAll flag if we update SecretRule to support it. + // TODO(generate): Return dotAll flag if we update SecretRule to support. } - + // 6. Handle Scoped Dot-all `(?s:...)` if (pattern.contains('(?s:')) { pattern = pattern.replaceAll('(?s:', '(?:'); } - + // 7. Clean up potential double-escapes or leftover artifacts if necessary. // (Currently not needed for standard Gitleaks patterns).