A CLI security inspection tool for PHP APIs. Performs static source-code analysis using nikic/php-parser to identify authorization misconfigurations and security risks in Laravel, Symfony, and Slim applications.
- Static analysis of PHP projects (no runtime required)
- Discovers endpoints from Laravel routes, Symfony attributes, and Slim route definitions
- Detects 8 common security issues with authorization
- Multiple output formats: Terminal, JSON, Markdown, HTML
- Sorting, filtering, and grouping of results
- Configuration file support with suppressions
- Accessibility options (no-color, no-icons)
- CI/CD integration with
--fail-onexit codes - Works with PHP 8.1+
# Install globally (recommended — scan any project from anywhere)
composer global require apiposture/apiposture
# Or install as a dev dependency inside your project
composer require --dev apiposture/apipostureGlobal install tip: After
composer global require, make sure Composer's global bin directory is on yourPATH. The location varies by system — find yours withcomposer global config bin-dir --absolute, then add it:# Modern Linux / macOS (Composer 2.x) export PATH="$PATH:$HOME/.config/composer/vendor/bin" # Older Linux / macOS export PATH="$PATH:$HOME/.composer/vendor/bin" # Add whichever applies to ~/.bashrc or ~/.zshrc to make it permanentThen run
apiposture scan /path/to/projectfrom anywhere.
Project install tip:
vendor/bin/apiposturemust be run from the directory where you rancomposer require(where thevendor/folder lives). Pass the target path as an argument:# Installed in ~/myapp, scanning a subdirectory cd ~/myapp vendor/bin/apiposture scan src/Controller # Scanning a completely separate project vendor/bin/apiposture scan /path/to/other-project
# Scan a project directory (global install)
apiposture scan /path/to/project
# Scan a project directory (project install — run from vendor root)
vendor/bin/apiposture scan /path/to/project
# Scan a subdirectory
vendor/bin/apiposture scan ./src/Controller
# Output as JSON
vendor/bin/apiposture scan . --output json
# Output as Markdown report
vendor/bin/apiposture scan . --output markdown --output-file report.md
# Output as HTML report
vendor/bin/apiposture scan . --output html --output-file report.html
# Filter by severity
vendor/bin/apiposture scan . --severity medium
# CI integration - fail if high severity findings
vendor/bin/apiposture scan . --fail-on high
# Sorting
vendor/bin/apiposture scan . --sort-by route --sort-dir asc
# Filtering
vendor/bin/apiposture scan . --classification public --method POST
vendor/bin/apiposture scan . --route-contains admin --controller UserController
# Grouping
vendor/bin/apiposture scan . --group-by controller
vendor/bin/apiposture scan . --group-findings-by severity
# Accessibility (no colors/icons)
vendor/bin/apiposture scan . --no-color --no-icons
# Use config file
vendor/bin/apiposture scan . --config .apiposture.jsonRoute::get(),Route::post(),Route::put(),Route::delete(),Route::patch()Route::middleware(['auth'])->group(...)with nested routesRoute::prefix('/api')->group(...)with path prefixes- Controller middleware via
$this->middleware('auth')in constructors #[Middleware('auth')]attributes (Laravel 11+)- Auth detection:
auth,auth:sanctum,auth:api,role:*,can:*,guest
#[Route('/path', methods: ['GET'])]attributes#[IsGranted('ROLE_ADMIN')]security attributes#[Security("is_granted('ROLE_USER')")]expressions- Class-level route prefixes and security inheritance
ROLE_*pattern detection
$app->get(),$app->post(),$app->put(),$app->delete(),$app->patch()$app->group('/prefix', ...)with nested routes->add(new AuthMiddleware())middleware chains- Auth middleware detection via naming conventions
Create .apiposture.json in your project root:
{
"severity": { "default": "low", "failOn": "high" },
"suppressions": [
{ "route": "/api/health", "ruleId": "AP001" },
{ "route": "/api/webhook/*", "ruleId": "AP002" }
],
"rules": {
"AP006": { "enabled": false }
},
"display": { "useColors": true, "useIcons": true }
}| Rule ID | Name | Severity | Description |
|---|---|---|---|
| AP001 | Public without explicit intent | Medium | Endpoint is publicly accessible without guest middleware or explicit marker |
| AP002 | AllowAnonymous on write | High* | Public POST/PUT/DELETE/PATCH operations |
| AP003 | Controller/action conflict | High | Action overrides controller-level auth with anonymous access |
| AP004 | Missing auth on writes | Critical | Write endpoint with zero authentication whatsoever |
| AP005 | Excessive role access | Medium | More than 3 roles on a single endpoint |
| AP006 | Weak role naming | Low | Generic role names like "user", "admin", "guest" |
| AP007 | Sensitive route keywords | High/Low | admin, debug, export, secret in routes (High without auth, Low with auth) |
| AP008 | Unprotected endpoint | High/Info | Endpoint with no auth middleware (High for writes, Info for reads) |
*AP002 severity adjusts dynamically: webhooks → Medium, auth/login endpoints → Low, analytics → Low
ApiPosture Scan Results
────────────────────────────────────────────────────────
Path: /path/to/project
Duration: 0.25s
Files: 15 scanned
Summary
+-------------+-------+
| Metric | Count |
+-------------+-------+
| Endpoints | 42 |
| Findings | 8 |
| Critical | 1 |
| High | 3 |
| Medium | 2 |
| Low | 2 |
+-------------+-------+
Endpoints
+---------------------+---------+----------------+----------------+------+
| Route | Methods | Classification | Controller | Auth |
+---------------------+---------+----------------+----------------+------+
| /api/users | GET | Authenticated | UserController | Yes |
| /api/admin | GET | Public | AdminController| No |
| /api/orders | POST | Role Restricted| OrderController| Yes |
+---------------------+---------+----------------+----------------+------+
Create .github/workflows/api-security-scan.yml:
name: API Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Scan API for security issues
run: vendor/bin/apiposture scan ./app --fail-on highname: API Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run security scan
id: scan
continue-on-error: true
run: |
vendor/bin/apiposture scan ./app \
--output json \
--output-file scan-results.json \
--fail-on high
- name: Generate Markdown report
if: always()
run: |
vendor/bin/apiposture scan ./app \
--output markdown \
--output-file api-security-report.md
- name: Upload JSON results
if: always()
uses: actions/upload-artifact@v4
with:
name: security-scan-json
path: scan-results.json
- name: Upload Markdown report
if: always()
uses: actions/upload-artifact@v4
with:
name: security-scan-report
path: api-security-report.md
- name: Comment PR with results
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('api-security-report.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## API Security Scan Results\n\n${report}`
});
- name: Fail if high severity issues found
if: steps.scan.outcome == 'failure'
run: exit 1--fail-on <severity>: Exit with code 1 if findings of specified severity or higher are found--output json: Generate machine-readable JSON output for further processing--output markdown: Generate human-readable Markdown reports--output html: Generate self-contained HTML reports--severity <level>: Set minimum severity level to report--config .apiposture.json: Use configuration file for suppressions and custom rules
0: Scan completed successfully with no findings above the fail threshold1: Findings above the fail threshold were detected, or error during scan
ApiPosture.php/
├── src/
│ ├── Core/
│ │ ├── Analyzer/ # Scan orchestrator
│ │ ├── Classification/ # Security classifier
│ │ ├── Config/ # Configuration loader
│ │ ├── Discovery/ # Laravel, Symfony, Slim discoverers
│ │ └── Model/ # Endpoint, Finding, ScanResult, Enums
│ ├── Rules/ # 8 security rules + engine
│ ├── Output/ # Terminal, JSON, Markdown formatters
│ └── Command/ # CLI scan command
├── tests/
│ ├── Core/
│ ├── Rules/
│ ├── Output/
│ ├── Command/
│ └── Fixtures/ # Laravel, Symfony, Slim sample code
├── bin/apiposture # CLI entry point
└── composer.json
# Clone and install
git clone https://github.com/BlagoCuljak/ApiPosture.php.git
cd ApiPosture.php
composer install
# Run tests
vendor/bin/phpunit
# Run against sample fixtures
vendor/bin/apiposture scan tests/Fixtures/Laravel
vendor/bin/apiposture scan tests/Fixtures/Symfony --output json
vendor/bin/apiposture scan tests/Fixtures/Slim --output markdown- ApiPosture - .NET version (ASP.NET Core)
- ApiPosture.Java - Java version (Spring Boot)
- ApiPosture.Python - Python version (Django, Flask, FastAPI)
- ApiPosture.Node.js - Node.js version (Express, Fastify, NestJS)
- ApiPosture.Go - Go version (Gin, Echo, Chi)
We welcome contributions! Please:
MIT

