diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..f717f74
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,51 @@
+---
+name: Bug Report
+about: Create a report to help us improve
+title: "[BUG] "
+labels: bug
+assignees: ''
+
+---
+
+## Bug Description
+
+A clear and concise description of what the bug is.
+
+## Environment
+
+**PHP Version:**
+
+
+**PHPStan Version:**
+
+
+**Extension Version:**
+
+
+## Code Sample
+
+Please provide a minimal code sample that demonstrates the issue:
+
+```php
+// Your code here that reproduces the issue
+```
+
+## Expected Behavior
+
+A clear and concise description of what you expected to happen.
+
+## Actual Behavior
+
+A clear and concise description of what actually happened.
+
+## PHPStan Configuration
+
+If relevant, please share your PHPStan configuration:
+
+```neon
+# Your phpstan.neon configuration
+```
+
+## Additional Context
+
+Add any other context about the problem here (stack traces, error messages, etc.).
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..28e7339
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Ask a Question
+ url: https://github.com/built-fast/phpstan-sensitive-parameter/discussions
+ about: Ask questions and discuss with the community
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..3ed2765
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,36 @@
+---
+name: Feature Request
+about: Suggest an idea for this project
+title: "[FEATURE] "
+labels: enhancement
+assignees: ''
+
+---
+
+## Feature Description
+
+A clear and concise description of what you want to happen.
+
+## Use Case
+
+Describe the problem you're trying to solve. Why would this feature be useful?
+
+## Proposed Solution
+
+A clear and concise description of what you want to happen.
+
+## Code Example
+
+If applicable, provide a code example of how this feature would work:
+
+```php
+// Example of how the feature would be used
+```
+
+## Alternatives Considered
+
+A clear and concise description of any alternative solutions or features you've considered.
+
+## Additional Context
+
+Add any other context, screenshots, or examples about the feature request here.
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..bc4d7dd
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,42 @@
+## Summary
+
+Describe what this PR does and why it's needed.
+
+## Type of Change
+
+- [ ] ๐ Bug fix (non-breaking change that fixes an issue)
+- [ ] โจ New feature (non-breaking change that adds functionality)
+- [ ] ๐ฅ Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] ๐ Documentation update
+- [ ] ๐จ Code style/formatting change
+- [ ] โป๏ธ Refactoring (no functional changes)
+- [ ] โก Performance improvement
+- [ ] ๐งช Test updates
+
+## Changes Made
+
+- [ ] Describe specific change 1
+- [ ] Describe specific change 2
+- [ ] Describe specific change 3
+
+## Testing
+
+- [ ] Added/updated tests for new functionality
+- [ ] All existing tests pass (`vendor/bin/pest`)
+- [ ] PHPStan analysis passes (`vendor/bin/phpstan analyze`)
+- [ ] Code style check passes (`vendor/bin/pint --test`)
+- [ ] Tested with PHP 8.2 and 8.3
+- [ ] Tested with multiple PHPStan versions (if applicable)
+
+## Documentation
+
+- [ ] README updated (if applicable)
+- [ ] Code comments added for complex logic
+- [ ] PHPDoc blocks updated
+
+## Checklist
+
+- [ ] Self-review completed
+- [ ] Code follows project style guidelines (PSR-12, enforced by Pint)
+- [ ] No breaking changes (or breaking changes documented)
+- [ ] Commit messages are clear and descriptive
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..82158cf
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,104 @@
+name: CI
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ name: Tests (PHP ${{ matrix.php-version }}, PHPStan ${{ matrix.phpstan-version }})
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php-version: [8.2, 8.3]
+ phpstan-version: ['2.0.*', '^2.1']
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: dom, curl, libxml, mbstring, zip
+ coverage: none
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Install specific PHPStan version
+ run: composer require --dev "phpstan/phpstan:${{ matrix.phpstan-version }}" --no-update && composer update phpstan/phpstan
+
+ - name: Execute tests via Pest
+ run: vendor/bin/pest
+
+ pint:
+ name: Code Style (Pint)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.3
+ extensions: dom, curl, libxml, mbstring, zip
+ coverage: none
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run Pint
+ run: vendor/bin/pint --test
+
+ phpstan:
+ name: Static Analysis (PHPStan)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.3
+ extensions: dom, curl, libxml, mbstring, zip
+ coverage: none
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run PHPStan
+ run: vendor/bin/phpstan analyze
diff --git a/.gitignore b/.gitignore
index 94d6d75..8a2b9f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
/vendor/
-/composer.lock
\ No newline at end of file
+/composer.lock
+/.envrc
+/.phpunit.result.cache
+/.claude/settings.local.json
+.DS_Store
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..27fc6cf
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 BuiltFast.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index cdf4708..bc3f138 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,176 @@
-# PHPStan Sensitive Parameter Detector
+# PHPStan SensitiveParameter Detector
-This PHPStan extension helps detect parameters that might contain sensitive information and should be marked with the `#[\SensitiveParameter]` attribute.
+[](https://github.com/built-fast/phpstan-sensitive-parameter/actions)
+[](https://packagist.org/packages/built-fast/phpstan-sensitive-parameter)
+[](https://packagist.org/packages/built-fast/phpstan-sensitive-parameter)
+[](https://packagist.org/packages/built-fast/phpstan-sensitive-parameter)
+
+A PHPStan extension that detects parameters that might contain sensitive information and should be marked with the `#[\SensitiveParameter]` attribute (added in PHP 8.2+).
+
+## About SensitiveParameter
+
+The `#[\SensitiveParameter]` attribute was introduced in PHP 8.2 to mark sensitive data that should be hidden from stack traces and debugging output. This extension helps you identify parameters that should use this attribute for better security.
+
+Learn more: [PHP RFC: Redact parameters in back traces](https://wiki.php.net/rfc/redact_parameters_in_back_traces)
+
+## Requirements
+
+- PHP 8.2 or higher
+- PHPStan 2.0 or higher
## Installation
```bash
-composer require built-fast/phpstan-sensitive-parameter
+composer require --dev built-fast/phpstan-sensitive-parameter
```
## Usage
-The extension will be automatically registered if you use [PHPStan's Composer plugin](https://github.com/phpstan/extension-installer).
+The extension will be automatically registered if you use [PHPStan's extension installer](https://github.com/phpstan/extension-installer).
Alternatively, include the extension in your PHPStan configuration:
```neon
includes:
- - vendor/built-fast/phpstan-sensitive-parameter/rules.neon
+ - vendor/built-fast/phpstan-sensitive-parameter/extension.neon
```
-## Configuration
+## What it detects
-The rule detects parameters with names containing common sensitive keywords like 'password', 'secret', 'token', etc.
+The rule detects parameters with names containing common sensitive keywords:
-You can customize the list of sensitive keywords by adding them in your `phpstan.neon` file:
+- Authentication: `password`, `secret`, `token`, `credential`, `auth`, `bearer`
+- API Security: `apikey` (matches `apisecret`, `clientsecret` via `secret`)
+- Financial: `credit`, `card`, `ccv`, `cvv`, `ssn`, `pin`
+- Security: `private`, `signature`, `hash`, `salt`, `nonce`, `otp`, `passcode`, `csrf`
-```neon
-parameters:
- sensitiveParameterDetector:
- additionalKeywords:
- - client
- - customer
- - account
- - user
+Note: Due to substring matching, `secret` catches `apisecret`/`clientsecret` and `token` catches `refreshtoken`/`accesstoken`.
+
+It works with:
+
+- Regular functions
+- Class methods (public, private, protected, static)
+- Constructors
+- Case-insensitive matching (`Password`, `SECRET`, etc.)
+- Partial matches (`userPassword`, `secretKey`, etc.)
+
+## Examples
+
+### โ Will trigger warnings:
+
+```php
+function login(string $username, string $password) {
+ // Parameter $password should use #[\SensitiveParameter]
+}
+
+class AuthService {
+ public function setCredentials(string $apikey, string $secret) {
+ // Both $apikey and $secret should be marked sensitive
+ }
+}
```
-## Example
+### โ
Properly protected:
```php
-// This will raise a PHPStan error
+// Function-level protection
+#[\SensitiveParameter]
function login(string $username, string $password) {
- // ...
+ // All parameters are protected
}
-// This is fine
-function login(string $username, #[\SensitiveParameter] string $password) {
- // ...
+// Parameter-level protection
+function authenticate(
+ string $username,
+ #[\SensitiveParameter] string $password
+) {
+ // Only $password is protected
+}
+
+// Mixed protection
+class AuthService {
+ public function verify(
+ #[\SensitiveParameter] string $token,
+ string $userId,
+ string $apikey // This will still trigger a warning
+ ) {
+ // $token is protected, $apikey needs protection
+ }
}
```
+
+## Advanced Configuration
+
+To use custom sensitive keywords instead of the defaults, override the service:
+
+```neon
+includes:
+ - vendor/built-fast/phpstan-sensitive-parameter/extension.neon
+
+services:
+ # Override the default service with custom keywords
+ -
+ class: BuiltFast\Rules\SensitiveParameterDetectorRule
+ arguments:
+ - ['password', 'apikey', 'token', 'banking', 'medical'] # Your custom keywords
+ tags:
+ - phpstan.rules.rule
+```
+
+This completely replaces the default keyword list with your own.
+
+## Suppressing Warnings
+
+You can suppress warnings using PHPStan's ignore comments:
+
+```php
+// @phpstan-ignore-next-line sensitiveParameter.missing
+function legacyFunction(string $password) {
+ // Legacy code that cannot be updated
+}
+
+// @phpstan-ignore-next-line sensitiveParameter.missing
+function anotherLegacyFunction(string $secret) {
+ // Another legacy function
+}
+
+function modernFunction(string $password): void // @phpstan-ignore-line sensitiveParameter.missing
+{
+ // Function with inline ignore comment
+}
+```
+
+## Reporting Issues
+
+Found a bug or have a feature request? Please [report it on GitHub](https://github.com/built-fast/phpstan-sensitive-parameter/issues).
+
+When reporting issues, please include:
+
+- PHP version
+- PHPStan version
+- Code sample that demonstrates the issue
+- Expected vs actual behavior
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
+
+**Development setup:**
+
+```bash
+git clone https://github.com/built-fast/phpstan-sensitive-parameter.git
+cd phpstan-sensitive-parameter
+composer install
+```
+
+**Running tests:**
+
+```bash
+vendor/bin/pest # Run tests
+vendor/bin/phpstan analyze # Static analysis
+vendor/bin/pint --test # Code style check
+```
+
+## License
+
+MIT License - see [`LICENSE`](./LICENSE) for details.
diff --git a/composer.json b/composer.json
index c01542a..59cdae8 100644
--- a/composer.json
+++ b/composer.json
@@ -1,11 +1,31 @@
{
"name": "built-fast/phpstan-sensitive-parameter",
- "description": "PHPStan rule to detect potential sensitive parameters",
+ "description": "PHPStan extension for detecting parameters that should use SensitiveParameter",
"type": "phpstan-extension",
"license": "MIT",
- "version": "1.0.0",
+ "homepage": "https://github.com/built-fast/phpstan-sensitive-parameter",
+ "keywords": [
+ "phpstan",
+ "phpstan-extension",
+ "static-analysis",
+ "security",
+ "sensitive-parameter",
+ "php",
+ "code-quality"
+ ],
+ "authors": [
+ {
+ "name": "Josh Priddle",
+ "homepage": "https://github.com/built-fast/phpstan-sensitive-parameter"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/built-fast/phpstan-sensitive-parameter/issues",
+ "forum": "https://github.com/built-fast/phpstan-sensitive-parameter/discussions",
+ "source": "https://github.com/built-fast/phpstan-sensitive-parameter"
+ },
"require": {
- "php": "^8.0",
+ "php": "^8.2",
"phpstan/phpstan": "^2.0"
},
"autoload": {
@@ -13,10 +33,15 @@
"BuiltFast\\": "src/"
}
},
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\": "tests/"
+ }
+ },
"extra": {
"phpstan": {
"includes": [
- "rules.neon"
+ "extension.neon"
]
}
},
@@ -28,5 +53,7 @@
"allow-plugins": {
"pestphp/pest-plugin": true
}
- }
+ },
+ "minimum-stability": "stable",
+ "prefer-stable": true
}
diff --git a/extension.neon b/extension.neon
new file mode 100644
index 0000000..48dafcc
--- /dev/null
+++ b/extension.neon
@@ -0,0 +1,5 @@
+services:
+ -
+ class: BuiltFast\Rules\SensitiveParameterDetectorRule
+ tags:
+ - phpstan.rules.rule
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..0146844
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,11 @@
+parameters:
+ level: 7
+ paths:
+ - src
+
+ # Ignore test fixtures since they're intentionally problematic
+ excludePaths:
+ - tests/Fixtures/*
+
+ # PHP version (match your composer.json requirement)
+ phpVersion: 80000
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
index 14e4fb4..f0b8e88 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -7,6 +7,9 @@
tests/Unit
+
+ tests/Feature
+
diff --git a/pint.json b/pint.json
index d4c8c61..c33a741 100644
--- a/pint.json
+++ b/pint.json
@@ -1,5 +1,5 @@
{
- "preset": "laravel",
+ "preset": "psr12",
"rules": {
"array_push": true,
"backtick_to_shell_exec": true,
@@ -10,7 +10,9 @@
"final_class": true,
"final_internal_class": true,
"final_public_method_for_abstract_class": true,
- "fully_qualified_strict_types": true,
+ "fully_qualified_strict_types": {
+ "import_symbols": true
+ },
"global_namespace_import": {
"import_classes": true,
"import_constants": true,
diff --git a/rules.neon b/rules.neon
deleted file mode 100644
index ff67a31..0000000
--- a/rules.neon
+++ /dev/null
@@ -1,11 +0,0 @@
-parameters:
- sensitiveParameterDetector:
- additionalKeywords: []
-
-services:
- -
- factory: BuiltFast\PHPStan\SensitiveParameterDetectorRuleFactory(
- %sensitiveParameterDetector.additionalKeywords%
- )
- tags:
- - phpstan.rules.rule
diff --git a/src/PHPStan/Rules/SensitiveParameterDetectorRule.php b/src/PHPStan/Rules/SensitiveParameterDetectorRule.php
deleted file mode 100644
index 1e32208..0000000
--- a/src/PHPStan/Rules/SensitiveParameterDetectorRule.php
+++ /dev/null
@@ -1,119 +0,0 @@
-sensitiveKeywords = array_merge(
- [
- 'password', 'secret', 'token', 'key', 'credential', 'auth',
- 'credit', 'card', 'ccv', 'cvv', 'ssn', 'api', 'private',
- ],
- $sensitiveKeywords
- );
- }
-
- public function getNodeType(): string
- {
- return FunctionLike::class;
- }
-
- /**
- * @return RuleError[]
- */
- public function processNode(Node $node, Scope $scope): array
- {
- $errors = [];
-
- if (! $node instanceof ClassMethod && ! $node instanceof Function_) {
- return $errors;
- }
-
- $hasSensitiveAttribute = false;
-
- foreach ($node->attrGroups as $attrGroup) {
- foreach ($attrGroup->attrs as $attr) {
- $attrName = $attr->name->toString();
- if (
- $attrName === 'SensitiveParameter' ||
- $attrName === '\SensitiveParameter' ||
- mb_strpos($attrName, 'SensitiveParameter') !== false
- ) {
- $hasSensitiveAttribute = true;
- break 2;
- }
- }
- }
-
- foreach ($node->getParams() as $param) {
- if (! $param->var instanceof Node\Expr\Variable || ! is_string($param->var->name)) {
- continue;
- }
-
- $paramName = $param->var->name;
-
- $paramHasSensitiveAttribute = false;
-
- if ($param->attrGroups) {
- foreach ($param->attrGroups as $attrGroup) {
- foreach ($attrGroup->attrs as $attr) {
- $attrName = $attr->name->toString();
- if (
- $attrName === 'SensitiveParameter' ||
- $attrName === '\SensitiveParameter' ||
- mb_strpos($attrName, 'SensitiveParameter') !== false
- ) {
- $paramHasSensitiveAttribute = true;
- break 2;
- }
- }
- }
- }
-
- if ($hasSensitiveAttribute || $paramHasSensitiveAttribute) {
- continue;
- }
-
- foreach ($this->sensitiveKeywords as $keyword) {
- if (mb_stripos($paramName, $keyword) !== false) {
- $functionName = $node instanceof ClassMethod
- ? ($scope->getClassReflection() ? $scope->getClassReflection()->getName().'::' : '').$node->name->name
- : $node->name->name;
-
- $errors[] = RuleErrorBuilder::message(sprintf(
- 'Parameter $%s in %s might contain sensitive information. Consider using #[\\SensitiveParameter] attribute.',
- $paramName,
- $functionName
- ))
- ->build();
- break;
- }
- }
- }
-
- return $errors;
- }
-}
diff --git a/src/PHPStan/Rules/SensitiveParameterDetectorRuleFactory.php b/src/PHPStan/Rules/SensitiveParameterDetectorRuleFactory.php
deleted file mode 100644
index b44bf59..0000000
--- a/src/PHPStan/Rules/SensitiveParameterDetectorRuleFactory.php
+++ /dev/null
@@ -1,26 +0,0 @@
-additionalKeywords = $additionalKeywords;
- }
-
- public function create(): SensitiveParameterDetectorRule
- {
- return new SensitiveParameterDetectorRule($this->additionalKeywords);
- }
-}
diff --git a/src/Rules/SensitiveParameterDetectorRule.php b/src/Rules/SensitiveParameterDetectorRule.php
new file mode 100644
index 0000000..9589d93
--- /dev/null
+++ b/src/Rules/SensitiveParameterDetectorRule.php
@@ -0,0 +1,199 @@
+
+ */
+final class SensitiveParameterDetectorRule implements Rule
+{
+ /** @var string[] Array of sensitive keywords to match against */
+ private array $sensitiveKeywords;
+
+ /**
+ * Initialize the rule with custom or default sensitive keywords.
+ *
+ * When custom keywords are provided, they completely replace the default
+ * keywords. When an empty array is provided, the default keywords are
+ * used.
+ *
+ * Default keywords include common sensitive terms like 'password',
+ * 'secret', 'token', 'apikey', 'credit', 'ssn', 'hash', and many others
+ * covering authentication, financial, and security-related parameters.
+ *
+ * Custom keywords should be lowercase strings that will be matched
+ * case-insensitively against parameter names using substring matching
+ * (e.g., 'secret' matches 'userSecret').
+ *
+ * @param string[] $sensitiveKeywords Array of custom keywords to use
+ * instead of defaults
+ */
+ public function __construct(array $sensitiveKeywords = [])
+ {
+ // Use provided keywords or fall back to defaults
+ $this->sensitiveKeywords = empty($sensitiveKeywords) ? [
+ 'apikey',
+ 'auth',
+ 'bearer',
+ 'card',
+ 'ccv',
+ 'credential',
+ 'credit',
+ 'csrf',
+ 'cvv',
+ 'hash',
+ 'nonce',
+ 'otp',
+ 'passcode',
+ 'password',
+ 'pin',
+ 'private',
+ 'salt',
+ 'secret',
+ 'signature',
+ 'ssn',
+ 'token',
+ ] : $sensitiveKeywords;
+ }
+
+ /**
+ * Specify that this rule processes function-like nodes (functions,
+ * methods, closures).
+ *
+ * This includes regular functions, class methods
+ * (public/private/protected/static), constructors, destructors, and
+ * closures.
+ *
+ * @return string The class name of nodes this rule processes
+ */
+ public function getNodeType(): string
+ {
+ return FunctionLike::class;
+ }
+
+ /**
+ * Analyze a function-like node to detect parameters that might need
+ * SensitiveParameter attribute.
+ *
+ * This method examines each parameter in the function/method to determine
+ * if:
+ *
+ * 1. The parameter name contains any sensitive keywords (case-insensitive
+ * substring match)
+ * 2. The parameter is not already protected by #[\SensitiveParameter]
+ * attribute
+ * 3. The entire function is not protected by #[\SensitiveParameter]
+ * attribute
+ *
+ * Protection can be applied at two levels:
+ *
+ * - Function level: #[\SensitiveParameter] before the function declaration
+ * protects all parameters
+ * - Parameter level: #[\SensitiveParameter] before individual parameter
+ * declarations
+ *
+ * @param Node $node The function/method node being analyzed
+ * @param Scope $scope PHPStan scope context for the analysis
+ *
+ * @return RuleError[] Array of rule violations found for unprotected
+ * sensitive parameters
+ */
+ public function processNode(Node $node, Scope $scope): array
+ {
+ $errors = [];
+
+ if (! $node instanceof ClassMethod && ! $node instanceof Function_) {
+ return $errors;
+ }
+
+ $hasSensitiveAttribute = false;
+
+ foreach ($node->attrGroups as $attrGroup) {
+ foreach ($attrGroup->attrs as $attr) {
+ $attrName = $attr->name->toString();
+ if (
+ $attrName === 'SensitiveParameter' ||
+ $attrName === '\SensitiveParameter' ||
+ mb_strpos($attrName, 'SensitiveParameter') !== false
+ ) {
+ $hasSensitiveAttribute = true;
+ break 2;
+ }
+ }
+ }
+
+ foreach ($node->getParams() as $param) {
+ if (! $param->var instanceof Node\Expr\Variable || ! is_string($param->var->name)) {
+ continue;
+ }
+
+ $paramName = $param->var->name;
+
+ $paramHasSensitiveAttribute = false;
+
+ if ($param->attrGroups) {
+ foreach ($param->attrGroups as $attrGroup) {
+ foreach ($attrGroup->attrs as $attr) {
+ $attrName = $attr->name->toString();
+ if (
+ $attrName === 'SensitiveParameter' ||
+ $attrName === '\SensitiveParameter' ||
+ mb_strpos($attrName, 'SensitiveParameter') !== false
+ ) {
+ $paramHasSensitiveAttribute = true;
+ break 2;
+ }
+ }
+ }
+ }
+
+ if ($hasSensitiveAttribute || $paramHasSensitiveAttribute) {
+ continue;
+ }
+
+ foreach ($this->sensitiveKeywords as $keyword) {
+ if (mb_stripos($paramName, $keyword) !== false) {
+ $functionName = $node instanceof ClassMethod
+ ? ($scope->getClassReflection() ? $scope->getClassReflection()->getName().'::' : '').$node->name->name
+ : $node->name->name;
+
+ $errors[] = RuleErrorBuilder::message(sprintf(
+ 'Parameter $%s in %s might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ $paramName,
+ $functionName
+ ))
+ ->identifier('sensitiveParameter.missing')
+ ->build();
+ break;
+ }
+ }
+ }
+
+ return $errors;
+ }
+}
diff --git a/tests/Feature/BasicSensitiveParametersTest.php b/tests/Feature/BasicSensitiveParametersTest.php
new file mode 100644
index 0000000..3c4f180
--- /dev/null
+++ b/tests/Feature/BasicSensitiveParametersTest.php
@@ -0,0 +1,168 @@
+rule = new SensitiveParameterDetectorRule();
+});
+
+it('detects basic sensitive parameters', function () {
+ $this->analyse([__DIR__.'/../Fixtures/BasicSensitiveParameters.php'], [
+ [
+ 'Parameter $userSecret in Tests\\Fixtures\\BasicSensitiveParameters::regularFunction might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 13,
+ ],
+ [
+ 'Parameter $credential in Tests\\Fixtures\\BasicSensitiveParameters::regularFunction might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 13,
+ ],
+ [
+ 'Parameter $password in Tests\\Fixtures\\BasicSensitiveParameters::authenticate might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 19,
+ ],
+ [
+ 'Parameter $apikey in Tests\\Fixtures\\BasicSensitiveParameters::setApiCredentials might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 24,
+ ],
+ [
+ 'Parameter $apisecret in Tests\\Fixtures\\BasicSensitiveParameters::setApiCredentials might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 24,
+ ],
+ [
+ 'Parameter $cardNumber in Tests\\Fixtures\\BasicSensitiveParameters::processPayment might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 29,
+ ],
+ [
+ 'Parameter $cvv in Tests\\Fixtures\\BasicSensitiveParameters::processPayment might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 29,
+ ],
+ [
+ 'Parameter $creditCardToken in Tests\\Fixtures\\BasicSensitiveParameters::processPayment might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 29,
+ ],
+ [
+ 'Parameter $ssn in Tests\\Fixtures\\BasicSensitiveParameters::storeUserData might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 34,
+ ],
+ [
+ 'Parameter $privateKey in Tests\\Fixtures\\BasicSensitiveParameters::storeUserData might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 34,
+ ],
+ [
+ 'Parameter $authToken in Tests\\Fixtures\\BasicSensitiveParameters::handleTokens might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 39,
+ ],
+ [
+ 'Parameter $refreshToken in Tests\\Fixtures\\BasicSensitiveParameters::handleTokens might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 39,
+ ],
+ [
+ 'Parameter $accessToken in Tests\\Fixtures\\BasicSensitiveParameters::handleTokens might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 39,
+ ],
+ [
+ 'Parameter $password in globalAuthFunction might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 46,
+ ],
+ ]);
+});
+
+test('does not warn for protected sensitive parameters', function () {
+ $this->analyse([__DIR__.'/../Fixtures/ProtectedSensitiveParameters.php'], [
+ [
+ 'Parameter $apikey in Tests\\Fixtures\\ProtectedSensitiveParameters::setApiCredentials might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 22,
+ ],
+ [
+ 'Parameter $cvv in Tests\\Fixtures\\ProtectedSensitiveParameters::processPayment might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 28,
+ ],
+ ]);
+});
+
+test('detects edge cases correctly', function () {
+ $this->analyse([__DIR__.'/../Fixtures/EdgeCases.php'], [
+ [
+ 'Parameter $apikey in Tests\\Fixtures\\EdgeCases::__construct might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 13,
+ ],
+ [
+ 'Parameter $credential in Tests\\Fixtures\\EdgeCases::staticMethod might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 18,
+ ],
+ [
+ 'Parameter $userPassword in Tests\\Fixtures\\EdgeCases::partialMatches might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 24,
+ ],
+ [
+ 'Parameter $secretKey in Tests\\Fixtures\\EdgeCases::partialMatches might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 24,
+ ],
+ [
+ 'Parameter $apiToken in Tests\\Fixtures\\EdgeCases::partialMatches might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 24,
+ ],
+ [
+ 'Parameter $Password in Tests\\Fixtures\\EdgeCases::caseVariations might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 30,
+ ],
+ [
+ 'Parameter $SECRET in Tests\\Fixtures\\EdgeCases::caseVariations might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 30,
+ ],
+ [
+ 'Parameter $Token in Tests\\Fixtures\\EdgeCases::caseVariations might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 30,
+ ],
+ [
+ 'Parameter $myPassword in Tests\\Fixtures\\EdgeCases::mixedCaseCompounds might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 36,
+ ],
+ [
+ 'Parameter $userSecret in Tests\\Fixtures\\EdgeCases::mixedCaseCompounds might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 36,
+ ],
+ [
+ 'Parameter $appToken in Tests\\Fixtures\\EdgeCases::mixedCaseCompounds might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 36,
+ ],
+ [
+ 'Parameter $passwordless in Tests\\Fixtures\\EdgeCases::falsePositives might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 42,
+ ],
+ [
+ 'Parameter $secretion in Tests\\Fixtures\\EdgeCases::falsePositives might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 42,
+ ],
+ [
+ 'Parameter $password in Tests\\Fixtures\\EdgeCases::multiplePasswords might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 49,
+ ],
+ [
+ 'Parameter $confirmPassword in Tests\\Fixtures\\EdgeCases::multiplePasswords might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 49,
+ ],
+ [
+ 'Parameter $oldPassword in Tests\\Fixtures\\EdgeCases::multiplePasswords might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 49,
+ ],
+ [
+ 'Parameter $password123 in Tests\\Fixtures\\EdgeCases::unusualCases might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 55,
+ ],
+ [
+ 'Parameter $password_hash in Tests\\Fixtures\\EdgeCases::unusualCases might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 55,
+ ],
+ [
+ 'Parameter $secret in Tests\\Fixtures\\EdgeCases::privateMethod might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 61,
+ ],
+ [
+ 'Parameter $token in Tests\\Fixtures\\EdgeCases::protectedMethod might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 66,
+ ],
+ ]);
+});
diff --git a/tests/Feature/CustomKeywordsTest.php b/tests/Feature/CustomKeywordsTest.php
new file mode 100644
index 0000000..8f18bc1
--- /dev/null
+++ b/tests/Feature/CustomKeywordsTest.php
@@ -0,0 +1,54 @@
+rule = new SensitiveParameterDetectorRule(['banking', 'medical']);
+});
+
+it('detects custom keywords correctly', function () {
+ $this->analyse([__DIR__.'/../Fixtures/CustomKeywords.php'], [
+ [
+ 'Parameter $banking in Tests\\Fixtures\\CustomKeywords::bankingMethod might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 16,
+ ],
+ [
+ 'Parameter $bankingInfo in Tests\\Fixtures\\CustomKeywords::bankingMethod might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 16,
+ ],
+ [
+ 'Parameter $medical in Tests\\Fixtures\\CustomKeywords::medicalMethod might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 21,
+ ],
+ [
+ 'Parameter $medicalRecord in Tests\\Fixtures\\CustomKeywords::medicalMethod might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 21,
+ ],
+ [
+ 'Parameter $banking in Tests\\Fixtures\\CustomKeywords::mixedKeywords might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 27,
+ ],
+ [
+ 'Parameter $userBanking in Tests\\Fixtures\\CustomKeywords::customCompounds might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 33,
+ ],
+ [
+ 'Parameter $patientMedical in Tests\\Fixtures\\CustomKeywords::customCompounds might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 33,
+ ],
+ [
+ 'Parameter $Banking in Tests\\Fixtures\\CustomKeywords::customCaseVariations might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 39,
+ ],
+ [
+ 'Parameter $MEDICAL in Tests\\Fixtures\\CustomKeywords::customCaseVariations might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 39,
+ ],
+ [
+ 'Parameter $medical in Tests\\Fixtures\\CustomKeywords::parameterProtectedCustom might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 58,
+ ],
+ ]);
+});
diff --git a/tests/Feature/IgnoreCommentsTest.php b/tests/Feature/IgnoreCommentsTest.php
new file mode 100644
index 0000000..f815190
--- /dev/null
+++ b/tests/Feature/IgnoreCommentsTest.php
@@ -0,0 +1,16 @@
+analyse([__DIR__.'/../Fixtures/IgnoreComments.php'], [
+ [
+ 'Parameter $password in IgnoreComments::normalFunction might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 8,
+ ],
+ [
+ 'Parameter $secret in IgnoreComments::anotherFunction might contain sensitive information. Add the #[\\SensitiveParameter] attribute or ignore with `@phpstan-ignore sensitiveParameter.missing`.',
+ 19,
+ ],
+ ]);
+});
diff --git a/tests/Fixtures/BasicSensitiveParameters.php b/tests/Fixtures/BasicSensitiveParameters.php
new file mode 100644
index 0000000..4882f40
--- /dev/null
+++ b/tests/Fixtures/BasicSensitiveParameters.php
@@ -0,0 +1,49 @@
+in('Feature');
+uses(TestCase::class)->in('Feature');
/*
|--------------------------------------------------------------------------
@@ -40,8 +41,3 @@
| global functions to help you to reduce the number of lines of code in your test files.
|
*/
-
-function something()
-{
- // ..
-}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..5ab281c
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,21 @@
+
+ */
+abstract class TestCase extends RuleTestCase
+{
+ protected SensitiveParameterDetectorRule $rule;
+
+ protected function getRule(): SensitiveParameterDetectorRule
+ {
+ return $this->rule ?? new SensitiveParameterDetectorRule();
+ }
+}
diff --git a/tests/Unit/PHPStan/Rules/SensitiveParameterDetectorRuleTest.php b/tests/Unit/PHPStan/Rules/SensitiveParameterDetectorRuleTest.php
deleted file mode 100644
index 302e62b..0000000
--- a/tests/Unit/PHPStan/Rules/SensitiveParameterDetectorRuleTest.php
+++ /dev/null
@@ -1,28 +0,0 @@
-toBeInstanceOf(PHPStan\Rules\Rule::class);
-});
-
-it('returns FunctionLike as node type', function () {
- expect((new SensitiveParameterDetectorRule())->getNodeType())->toBe(PhpParser\Node\FunctionLike::class);
-});
-
-it('validates sensitive keywords from the rule', function () {
- $rule = new SensitiveParameterDetectorRule();
- $reflection = new ReflectionClass($rule);
- $property = $reflection->getProperty('sensitiveKeywords');
- $property->setAccessible(true);
- $sensitiveKeywords = $property->getValue($rule);
-
- expect($sensitiveKeywords)->toContain('password');
- expect($sensitiveKeywords)->toContain('secret');
- expect($sensitiveKeywords)->toContain('token');
- expect($sensitiveKeywords)->toContain('key');
-
- expect(count($sensitiveKeywords))->toBeGreaterThan(5);
-});
diff --git a/tests/Unit/Rules/SensitiveParameterDetectorRuleTest.php b/tests/Unit/Rules/SensitiveParameterDetectorRuleTest.php
new file mode 100644
index 0000000..534b723
--- /dev/null
+++ b/tests/Unit/Rules/SensitiveParameterDetectorRuleTest.php
@@ -0,0 +1,122 @@
+toBeInstanceOf(Rule::class);
+});
+
+it('returns FunctionLike as node type', function () {
+ expect((new SensitiveParameterDetectorRule())->getNodeType())->toBe(FunctionLike::class);
+});
+
+it('validates sensitive keywords from the rule', function () {
+ $rule = new SensitiveParameterDetectorRule();
+ $reflection = new ReflectionClass($rule);
+ $property = $reflection->getProperty('sensitiveKeywords');
+ $property->setAccessible(true);
+ $sensitiveKeywords = $property->getValue($rule);
+
+ expect($sensitiveKeywords)->toContain('password');
+ expect($sensitiveKeywords)->toContain('secret');
+ expect($sensitiveKeywords)->toContain('token');
+ expect($sensitiveKeywords)->toContain('apikey');
+
+ expect(count($sensitiveKeywords))->toBeGreaterThan(5);
+});
+
+it('uses default keywords when no custom keywords provided', function () {
+ $rule = new SensitiveParameterDetectorRule([]);
+ $reflection = new ReflectionClass($rule);
+ $property = $reflection->getProperty('sensitiveKeywords');
+ $property->setAccessible(true);
+ $sensitiveKeywords = $property->getValue($rule);
+
+ expect($sensitiveKeywords)->toContain('password');
+ expect($sensitiveKeywords)->toContain('secret');
+ expect($sensitiveKeywords)->toContain('token');
+});
+
+it('overrides default keywords with custom keywords', function () {
+ $customKeywords = ['custom1', 'custom2'];
+ $rule = new SensitiveParameterDetectorRule($customKeywords);
+ $reflection = new ReflectionClass($rule);
+ $property = $reflection->getProperty('sensitiveKeywords');
+ $property->setAccessible(true);
+ $sensitiveKeywords = $property->getValue($rule);
+
+ expect($sensitiveKeywords)->toBe($customKeywords);
+ expect($sensitiveKeywords)->not->toContain('password');
+ expect($sensitiveKeywords)->toContain('custom1');
+ expect($sensitiveKeywords)->toContain('custom2');
+});
+
+it('accepts empty custom keywords array', function () {
+ $rule = new SensitiveParameterDetectorRule([]);
+
+ expect($rule)->toBeInstanceOf(SensitiveParameterDetectorRule::class);
+});
+
+it('maintains case sensitivity for custom keywords', function () {
+ $customKeywords = ['Password', 'SECRET', 'Token'];
+ $rule = new SensitiveParameterDetectorRule($customKeywords);
+ $reflection = new ReflectionClass($rule);
+ $property = $reflection->getProperty('sensitiveKeywords');
+ $property->setAccessible(true);
+ $sensitiveKeywords = $property->getValue($rule);
+
+ expect($sensitiveKeywords)->toContain('Password');
+ expect($sensitiveKeywords)->toContain('SECRET');
+ expect($sensitiveKeywords)->toContain('Token');
+});
+
+it('handles duplicate keywords in custom array', function () {
+ $customKeywords = ['password', 'password', 'secret'];
+ $rule = new SensitiveParameterDetectorRule($customKeywords);
+ $reflection = new ReflectionClass($rule);
+ $property = $reflection->getProperty('sensitiveKeywords');
+ $property->setAccessible(true);
+ $sensitiveKeywords = $property->getValue($rule);
+
+ expect($sensitiveKeywords)->toBe($customKeywords); // Should preserve original array
+});
+
+it('has comprehensive default keyword coverage', function () {
+ $rule = new SensitiveParameterDetectorRule();
+ $reflection = new ReflectionClass($rule);
+ $property = $reflection->getProperty('sensitiveKeywords');
+ $property->setAccessible(true);
+ $keywords = $property->getValue($rule);
+
+ // Authentication keywords
+ expect($keywords)->toContain('password');
+ expect($keywords)->toContain('secret');
+ expect($keywords)->toContain('token');
+ expect($keywords)->toContain('credential');
+ expect($keywords)->toContain('auth');
+ expect($keywords)->toContain('bearer');
+
+ // API Security keywords
+ expect($keywords)->toContain('apikey');
+
+ // Financial keywords
+ expect($keywords)->toContain('credit');
+ expect($keywords)->toContain('card');
+ expect($keywords)->toContain('cvv');
+ expect($keywords)->toContain('ssn');
+ expect($keywords)->toContain('pin');
+
+ // Security keywords
+ expect($keywords)->toContain('private');
+ expect($keywords)->toContain('signature');
+ expect($keywords)->toContain('hash');
+ expect($keywords)->toContain('salt');
+ expect($keywords)->toContain('nonce');
+ expect($keywords)->toContain('otp');
+ expect($keywords)->toContain('passcode');
+ expect($keywords)->toContain('csrf');
+});