@apiposture/cli 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.apiposture.json.example +56 -0
- package/.github/workflows/publish.yml +38 -0
- package/.github/workflows/test.yml +42 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/cli/commands/license/activate.d.ts +3 -0
- package/dist/cli/commands/license/activate.js +35 -0
- package/dist/cli/commands/license/deactivate.d.ts +3 -0
- package/dist/cli/commands/license/deactivate.js +28 -0
- package/dist/cli/commands/license/status.d.ts +3 -0
- package/dist/cli/commands/license/status.js +36 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.js +211 -0
- package/dist/cli/options.d.ts +27 -0
- package/dist/cli/options.js +30 -0
- package/dist/core/analysis/project-analyzer.d.ts +16 -0
- package/dist/core/analysis/project-analyzer.js +54 -0
- package/dist/core/analysis/source-file-loader.d.ts +32 -0
- package/dist/core/analysis/source-file-loader.js +155 -0
- package/dist/core/authorization/authorization-extractor.d.ts +11 -0
- package/dist/core/authorization/authorization-extractor.js +2 -0
- package/dist/core/authorization/express-auth-extractor.d.ts +10 -0
- package/dist/core/authorization/express-auth-extractor.js +106 -0
- package/dist/core/authorization/global-auth-analyzer.d.ts +12 -0
- package/dist/core/authorization/global-auth-analyzer.js +74 -0
- package/dist/core/authorization/nestjs-auth-extractor.d.ts +13 -0
- package/dist/core/authorization/nestjs-auth-extractor.js +142 -0
- package/dist/core/configuration/config-loader.d.ts +27 -0
- package/dist/core/configuration/config-loader.js +72 -0
- package/dist/core/configuration/suppression-matcher.d.ts +14 -0
- package/dist/core/configuration/suppression-matcher.js +79 -0
- package/dist/core/discovery/discoverer-interface.d.ts +7 -0
- package/dist/core/discovery/discoverer-interface.js +2 -0
- package/dist/core/discovery/express-discoverer.d.ts +20 -0
- package/dist/core/discovery/express-discoverer.js +223 -0
- package/dist/core/discovery/fastify-discoverer.d.ts +19 -0
- package/dist/core/discovery/fastify-discoverer.js +249 -0
- package/dist/core/discovery/framework-detector.d.ts +9 -0
- package/dist/core/discovery/framework-detector.js +61 -0
- package/dist/core/discovery/index.d.ts +8 -0
- package/dist/core/discovery/index.js +8 -0
- package/dist/core/discovery/koa-discoverer.d.ts +16 -0
- package/dist/core/discovery/koa-discoverer.js +151 -0
- package/dist/core/discovery/nestjs-discoverer.d.ts +16 -0
- package/dist/core/discovery/nestjs-discoverer.js +180 -0
- package/dist/core/discovery/route-group-registry.d.ts +18 -0
- package/dist/core/discovery/route-group-registry.js +50 -0
- package/dist/core/licensing/license-context.d.ts +17 -0
- package/dist/core/licensing/license-context.js +15 -0
- package/dist/core/licensing/license-features.d.ts +14 -0
- package/dist/core/licensing/license-features.js +47 -0
- package/dist/core/models/authorization-info.d.ts +13 -0
- package/dist/core/models/authorization-info.js +25 -0
- package/dist/core/models/endpoint-type.d.ts +8 -0
- package/dist/core/models/endpoint-type.js +12 -0
- package/dist/core/models/endpoint.d.ts +16 -0
- package/dist/core/models/endpoint.js +16 -0
- package/dist/core/models/finding.d.ts +19 -0
- package/dist/core/models/finding.js +8 -0
- package/dist/core/models/http-method.d.ts +14 -0
- package/dist/core/models/http-method.js +25 -0
- package/dist/core/models/index.d.ts +10 -0
- package/dist/core/models/index.js +10 -0
- package/dist/core/models/scan-result.d.ts +21 -0
- package/dist/core/models/scan-result.js +35 -0
- package/dist/core/models/security-classification.d.ts +8 -0
- package/dist/core/models/security-classification.js +12 -0
- package/dist/core/models/severity.d.ts +11 -0
- package/dist/core/models/severity.js +23 -0
- package/dist/core/models/source-location.d.ts +7 -0
- package/dist/core/models/source-location.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/licensing/license-manager.d.ts +38 -0
- package/dist/licensing/license-manager.js +184 -0
- package/dist/output/accessibility-helper.d.ts +22 -0
- package/dist/output/accessibility-helper.js +98 -0
- package/dist/output/formatter-interface.d.ts +11 -0
- package/dist/output/formatter-interface.js +2 -0
- package/dist/output/index.d.ts +6 -0
- package/dist/output/index.js +6 -0
- package/dist/output/json-formatter.d.ts +7 -0
- package/dist/output/json-formatter.js +72 -0
- package/dist/output/markdown-formatter.d.ts +10 -0
- package/dist/output/markdown-formatter.js +114 -0
- package/dist/output/terminal-formatter.d.ts +12 -0
- package/dist/output/terminal-formatter.js +82 -0
- package/dist/rules/consistency/controller-action-conflict.d.ts +19 -0
- package/dist/rules/consistency/controller-action-conflict.js +40 -0
- package/dist/rules/consistency/missing-auth-on-writes.d.ts +21 -0
- package/dist/rules/consistency/missing-auth-on-writes.js +59 -0
- package/dist/rules/exposure/allow-anonymous-on-write.d.ts +20 -0
- package/dist/rules/exposure/allow-anonymous-on-write.js +42 -0
- package/dist/rules/exposure/public-without-explicit-intent.d.ts +20 -0
- package/dist/rules/exposure/public-without-explicit-intent.js +58 -0
- package/dist/rules/index.d.ts +11 -0
- package/dist/rules/index.js +11 -0
- package/dist/rules/privilege/excessive-role-access.d.ts +20 -0
- package/dist/rules/privilege/excessive-role-access.js +36 -0
- package/dist/rules/privilege/weak-role-naming.d.ts +20 -0
- package/dist/rules/privilege/weak-role-naming.js +50 -0
- package/dist/rules/rule-engine.d.ts +15 -0
- package/dist/rules/rule-engine.js +52 -0
- package/dist/rules/rule-interface.d.ts +16 -0
- package/dist/rules/rule-interface.js +2 -0
- package/dist/rules/surface/sensitive-route-keywords.d.ts +20 -0
- package/dist/rules/surface/sensitive-route-keywords.js +63 -0
- package/dist/rules/surface/unprotected-endpoint.d.ts +20 -0
- package/dist/rules/surface/unprotected-endpoint.js +61 -0
- package/package.json +60 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": {
|
|
3
|
+
"AP001": {
|
|
4
|
+
"enabled": true
|
|
5
|
+
},
|
|
6
|
+
"AP002": {
|
|
7
|
+
"enabled": true
|
|
8
|
+
},
|
|
9
|
+
"AP003": {
|
|
10
|
+
"enabled": true
|
|
11
|
+
},
|
|
12
|
+
"AP004": {
|
|
13
|
+
"enabled": true
|
|
14
|
+
},
|
|
15
|
+
"AP005": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"options": {
|
|
18
|
+
"maxRoles": 3
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"AP006": {
|
|
22
|
+
"enabled": true
|
|
23
|
+
},
|
|
24
|
+
"AP007": {
|
|
25
|
+
"enabled": true
|
|
26
|
+
},
|
|
27
|
+
"AP008": {
|
|
28
|
+
"enabled": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"suppressions": [
|
|
32
|
+
{
|
|
33
|
+
"ruleId": "AP001",
|
|
34
|
+
"route": "/api/health",
|
|
35
|
+
"reason": "Health check endpoint is intentionally public"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"ruleId": "AP002",
|
|
39
|
+
"routePattern": "/api/public/*",
|
|
40
|
+
"reason": "Public API routes are intentionally unauthenticated"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"output": {
|
|
44
|
+
"format": "terminal",
|
|
45
|
+
"noColor": false,
|
|
46
|
+
"noIcons": false
|
|
47
|
+
},
|
|
48
|
+
"scan": {
|
|
49
|
+
"excludePatterns": [
|
|
50
|
+
"**/node_modules/**",
|
|
51
|
+
"**/dist/**",
|
|
52
|
+
"**/*.test.ts",
|
|
53
|
+
"**/*.spec.ts"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout repository
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Node.js
|
|
20
|
+
uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: '20.x'
|
|
23
|
+
registry-url: 'https://registry.npmjs.org'
|
|
24
|
+
cache: 'npm'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: npm run build
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: npm test --if-present
|
|
34
|
+
|
|
35
|
+
- name: Publish to npm
|
|
36
|
+
run: npm publish --provenance --access public
|
|
37
|
+
env:
|
|
38
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [18.x, 20.x, 22.x]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: ${{ matrix.node-version }}
|
|
25
|
+
cache: 'npm'
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: npm ci
|
|
29
|
+
|
|
30
|
+
- name: Run linter
|
|
31
|
+
run: npm run lint --if-present
|
|
32
|
+
|
|
33
|
+
- name: Build
|
|
34
|
+
run: npm run build
|
|
35
|
+
|
|
36
|
+
- name: Run tests
|
|
37
|
+
run: npm test --if-present
|
|
38
|
+
|
|
39
|
+
- name: Test CLI execution
|
|
40
|
+
run: |
|
|
41
|
+
node dist/index.js --version
|
|
42
|
+
node dist/index.js scan samples/express-sample -o json > /dev/null
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BlagoCuljak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# ApiPosture CLI for Node.js
|
|
2
|
+
|
|
3
|
+
Static source-code analysis CLI for Node.js API frameworks to identify authorization misconfigurations and security risks.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-Framework Support**: Express.js, NestJS, Fastify, and Koa
|
|
8
|
+
- **8 Security Rules**: Covering exposure, consistency, privilege, and surface area risks
|
|
9
|
+
- **Multiple Output Formats**: Terminal, JSON, and Markdown
|
|
10
|
+
- **Configurable**: Rule customization and suppression support
|
|
11
|
+
- **CI/CD Ready**: Exit codes for pipeline integration
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @apiposture/cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or use with npx:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @apiposture/cli scan .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Scan current directory
|
|
29
|
+
apiposture scan
|
|
30
|
+
|
|
31
|
+
# Scan specific path
|
|
32
|
+
apiposture scan ./src
|
|
33
|
+
|
|
34
|
+
# Output as JSON
|
|
35
|
+
apiposture scan -o json
|
|
36
|
+
|
|
37
|
+
# Fail CI if critical findings
|
|
38
|
+
apiposture scan --fail-on critical
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Security Rules
|
|
42
|
+
|
|
43
|
+
| Rule | Name | Severity | Description |
|
|
44
|
+
|------|------|----------|-------------|
|
|
45
|
+
| AP001 | Public without explicit intent | High | Endpoint is public without @Public or allowAnonymous marker |
|
|
46
|
+
| AP002 | AllowAnonymous on write | High | Write operation explicitly marked as public |
|
|
47
|
+
| AP003 | Controller/action conflict | Medium | Method @Public overrides class-level guards (NestJS) |
|
|
48
|
+
| AP004 | Missing auth on writes | Critical | Unprotected write endpoint |
|
|
49
|
+
| AP005 | Excessive role access | Low | Endpoint allows >3 roles |
|
|
50
|
+
| AP006 | Weak role naming | Low | Generic role names like "admin", "user" |
|
|
51
|
+
| AP007 | Sensitive route keywords | Medium | Public route contains admin/debug/internal |
|
|
52
|
+
| AP008 | Unprotected endpoint | High | No middleware chain at all |
|
|
53
|
+
|
|
54
|
+
## CLI Options
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
apiposture scan [path]
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
-o, --output <format> Output format: terminal, json, markdown (default: terminal)
|
|
61
|
+
-f, --output-file <path> Write output to file
|
|
62
|
+
-c, --config <path> Config file path (.apiposture.json)
|
|
63
|
+
--severity <level> Min severity: info, low, medium, high, critical
|
|
64
|
+
--fail-on <level> Exit code 1 if findings at this level
|
|
65
|
+
--sort-by <field> Sort by: severity, route, method, classification
|
|
66
|
+
--sort-dir <dir> Sort direction: asc, desc
|
|
67
|
+
--classification <types> Filter: public, authenticated, role-restricted, policy-restricted
|
|
68
|
+
--method <methods> Filter: GET, POST, PUT, DELETE, PATCH
|
|
69
|
+
--route-contains <str> Filter routes containing string
|
|
70
|
+
--api-style <styles> Filter: express, nestjs, fastify, koa
|
|
71
|
+
--rule <rules> Filter by rule ID (comma-separated)
|
|
72
|
+
--no-color Disable colors
|
|
73
|
+
--no-icons Disable icons
|
|
74
|
+
|
|
75
|
+
License Commands:
|
|
76
|
+
apiposture license activate <key> Activate a license
|
|
77
|
+
apiposture license deactivate Deactivate current license
|
|
78
|
+
apiposture license status Show license status
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
Create `.apiposture.json` in your project root:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"rules": {
|
|
88
|
+
"AP001": { "enabled": true },
|
|
89
|
+
"AP005": { "enabled": true, "options": { "maxRoles": 3 } }
|
|
90
|
+
},
|
|
91
|
+
"suppressions": [
|
|
92
|
+
{
|
|
93
|
+
"ruleId": "AP001",
|
|
94
|
+
"route": "/api/health",
|
|
95
|
+
"reason": "Health check is intentionally public"
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"scan": {
|
|
99
|
+
"excludePatterns": ["**/test/**"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Supported Frameworks
|
|
105
|
+
|
|
106
|
+
### Express.js
|
|
107
|
+
```javascript
|
|
108
|
+
app.get('/path', handler);
|
|
109
|
+
router.post('/path', authMiddleware, handler);
|
|
110
|
+
app.use('/prefix', router);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### NestJS
|
|
114
|
+
```typescript
|
|
115
|
+
@Controller('path')
|
|
116
|
+
@UseGuards(AuthGuard)
|
|
117
|
+
class MyController {
|
|
118
|
+
@Get()
|
|
119
|
+
@Roles('admin')
|
|
120
|
+
handler() {}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Fastify
|
|
125
|
+
```javascript
|
|
126
|
+
fastify.get('/path', { preHandler: [auth] }, handler);
|
|
127
|
+
fastify.route({ method: 'GET', url: '/path', handler });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Koa
|
|
131
|
+
```javascript
|
|
132
|
+
router.get('/path', authMiddleware, handler);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## CI/CD Integration
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
# GitHub Actions
|
|
139
|
+
- name: Security Scan
|
|
140
|
+
run: npx @apiposture/cli scan --fail-on high -o json -f report.json
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
# GitLab CI
|
|
145
|
+
security-scan:
|
|
146
|
+
script:
|
|
147
|
+
- npx @apiposture/cli scan --fail-on critical
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Environment Variables
|
|
151
|
+
|
|
152
|
+
- `APIPOSTURE_LICENSE_KEY`: License key for Pro features
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { LicenseManager } from '../../../licensing/license-manager.js';
|
|
4
|
+
export function createActivateCommand() {
|
|
5
|
+
return new Command('activate')
|
|
6
|
+
.description('Activate a license key')
|
|
7
|
+
.argument('<key>', 'License key to activate')
|
|
8
|
+
.action(async (key) => {
|
|
9
|
+
const spinner = ora('Activating license...').start();
|
|
10
|
+
try {
|
|
11
|
+
const manager = new LicenseManager();
|
|
12
|
+
const result = await manager.activate(key);
|
|
13
|
+
if (result.success) {
|
|
14
|
+
spinner.succeed('License activated successfully!');
|
|
15
|
+
console.log(`\nLicense Type: ${result.licenseType}`);
|
|
16
|
+
console.log(`Expires: ${result.expiresAt?.toLocaleDateString() ?? 'Never'}`);
|
|
17
|
+
console.log('\nEnabled features:');
|
|
18
|
+
for (const feature of result.features ?? []) {
|
|
19
|
+
console.log(` - ${feature}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
spinner.fail('License activation failed');
|
|
24
|
+
console.error(`Error: ${result.error}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
spinner.fail('License activation failed');
|
|
30
|
+
console.error(error instanceof Error ? error.message : error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=activate.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { LicenseManager } from '../../../licensing/license-manager.js';
|
|
4
|
+
export function createDeactivateCommand() {
|
|
5
|
+
return new Command('deactivate')
|
|
6
|
+
.description('Deactivate the current license')
|
|
7
|
+
.action(async () => {
|
|
8
|
+
const spinner = ora('Deactivating license...').start();
|
|
9
|
+
try {
|
|
10
|
+
const manager = new LicenseManager();
|
|
11
|
+
const result = await manager.deactivate();
|
|
12
|
+
if (result.success) {
|
|
13
|
+
spinner.succeed('License deactivated successfully');
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
spinner.fail('License deactivation failed');
|
|
17
|
+
console.error(`Error: ${result.error}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
spinner.fail('License deactivation failed');
|
|
23
|
+
console.error(error instanceof Error ? error.message : error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=deactivate.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { LicenseManager } from '../../../licensing/license-manager.js';
|
|
3
|
+
export function createStatusCommand() {
|
|
4
|
+
return new Command('status')
|
|
5
|
+
.description('Show current license status')
|
|
6
|
+
.action(async () => {
|
|
7
|
+
try {
|
|
8
|
+
const manager = new LicenseManager();
|
|
9
|
+
const status = await manager.getStatus();
|
|
10
|
+
console.log('\nLicense Status');
|
|
11
|
+
console.log('==============');
|
|
12
|
+
if (status.isActive) {
|
|
13
|
+
console.log(`Status: Active`);
|
|
14
|
+
console.log(`Type: ${status.licenseType}`);
|
|
15
|
+
console.log(`Expires: ${status.expiresAt?.toLocaleDateString() ?? 'Never'}`);
|
|
16
|
+
console.log('\nEnabled Features:');
|
|
17
|
+
for (const feature of status.features ?? []) {
|
|
18
|
+
console.log(` [x] ${feature}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log('Status: Not licensed (Community Edition)');
|
|
23
|
+
console.log('\nTo unlock Pro features, activate a license key:');
|
|
24
|
+
console.log(' apiposture license activate <key>');
|
|
25
|
+
console.log('\nOr set the APIPOSTURE_LICENSE_KEY environment variable.');
|
|
26
|
+
}
|
|
27
|
+
console.log('');
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error('Failed to get license status');
|
|
31
|
+
console.error(error instanceof Error ? error.message : error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { defaultScanOptions, parseClassificationList, parseMethodList, parseApiStyleList, parseRuleList, } from '../options.js';
|
|
6
|
+
import { ProjectAnalyzer } from '../../core/analysis/project-analyzer.js';
|
|
7
|
+
import { ExpressDiscoverer } from '../../core/discovery/express-discoverer.js';
|
|
8
|
+
import { NestJSDiscoverer } from '../../core/discovery/nestjs-discoverer.js';
|
|
9
|
+
import { FastifyDiscoverer } from '../../core/discovery/fastify-discoverer.js';
|
|
10
|
+
import { KoaDiscoverer } from '../../core/discovery/koa-discoverer.js';
|
|
11
|
+
import { RuleEngine } from '../../rules/rule-engine.js';
|
|
12
|
+
import { getHighestSeverity } from '../../core/models/scan-result.js';
|
|
13
|
+
import { severityOrder, parseSeverity } from '../../core/models/severity.js';
|
|
14
|
+
import { TerminalFormatter } from '../../output/terminal-formatter.js';
|
|
15
|
+
import { JsonFormatter } from '../../output/json-formatter.js';
|
|
16
|
+
import { MarkdownFormatter } from '../../output/markdown-formatter.js';
|
|
17
|
+
import { ConfigLoader } from '../../core/configuration/config-loader.js';
|
|
18
|
+
export function createScanCommand() {
|
|
19
|
+
const command = new Command('scan')
|
|
20
|
+
.description('Scan a Node.js project for API security issues')
|
|
21
|
+
.argument('[path]', 'Path to the project to scan', '.')
|
|
22
|
+
.option('-o, --output <format>', 'Output format: terminal, json, markdown', 'terminal')
|
|
23
|
+
.option('-f, --output-file <path>', 'Write output to file')
|
|
24
|
+
.option('-c, --config <path>', 'Path to config file (.apiposture.json)')
|
|
25
|
+
.option('--severity <level>', 'Minimum severity: info, low, medium, high, critical')
|
|
26
|
+
.option('--fail-on <level>', 'Exit with code 1 if findings at this level or higher')
|
|
27
|
+
.option('--sort-by <field>', 'Sort by: severity, route, method, classification')
|
|
28
|
+
.option('--sort-dir <dir>', 'Sort direction: asc, desc')
|
|
29
|
+
.option('--classification <types>', 'Filter by classification (comma-separated)')
|
|
30
|
+
.option('--method <methods>', 'Filter by HTTP method (comma-separated)')
|
|
31
|
+
.option('--route-contains <str>', 'Filter routes containing string')
|
|
32
|
+
.option('--api-style <styles>', 'Filter by framework: express, nestjs, fastify, koa')
|
|
33
|
+
.option('--rule <rules>', 'Filter by rule ID (comma-separated)')
|
|
34
|
+
.option('--group-by <field>', 'Group endpoints by field')
|
|
35
|
+
.option('--no-color', 'Disable colors in output')
|
|
36
|
+
.option('--no-icons', 'Disable icons in output')
|
|
37
|
+
.action(async (projectPath, cmdOptions) => {
|
|
38
|
+
await runScan(projectPath, cmdOptions);
|
|
39
|
+
});
|
|
40
|
+
return command;
|
|
41
|
+
}
|
|
42
|
+
async function runScan(projectPath, cmdOptions) {
|
|
43
|
+
const absolutePath = path.resolve(projectPath);
|
|
44
|
+
// Validate project path exists
|
|
45
|
+
if (!fs.existsSync(absolutePath)) {
|
|
46
|
+
console.error(`Error: Path not found: ${absolutePath}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// Load config if specified (for future use with rule configuration)
|
|
50
|
+
const configPath = cmdOptions.config;
|
|
51
|
+
if (configPath) {
|
|
52
|
+
const configLoader = new ConfigLoader();
|
|
53
|
+
await configLoader.load(configPath);
|
|
54
|
+
}
|
|
55
|
+
// Parse options
|
|
56
|
+
const options = {
|
|
57
|
+
...defaultScanOptions,
|
|
58
|
+
output: cmdOptions.output ?? 'terminal',
|
|
59
|
+
outputFile: cmdOptions.outputFile,
|
|
60
|
+
severity: cmdOptions.severity ? parseSeverity(cmdOptions.severity) : undefined,
|
|
61
|
+
failOn: cmdOptions.failOn ? parseSeverity(cmdOptions.failOn) : undefined,
|
|
62
|
+
sortBy: cmdOptions.sortBy,
|
|
63
|
+
sortDir: cmdOptions.sortDir,
|
|
64
|
+
classification: cmdOptions.classification
|
|
65
|
+
? parseClassificationList(cmdOptions.classification)
|
|
66
|
+
: undefined,
|
|
67
|
+
method: cmdOptions.method
|
|
68
|
+
? parseMethodList(cmdOptions.method)
|
|
69
|
+
: undefined,
|
|
70
|
+
routeContains: cmdOptions.routeContains,
|
|
71
|
+
apiStyle: cmdOptions.apiStyle
|
|
72
|
+
? parseApiStyleList(cmdOptions.apiStyle)
|
|
73
|
+
: undefined,
|
|
74
|
+
rule: cmdOptions.rule
|
|
75
|
+
? parseRuleList(cmdOptions.rule)
|
|
76
|
+
: undefined,
|
|
77
|
+
noColor: cmdOptions.color === false,
|
|
78
|
+
noIcons: cmdOptions.icons === false,
|
|
79
|
+
};
|
|
80
|
+
// Start scanning
|
|
81
|
+
const spinner = ora({
|
|
82
|
+
text: 'Scanning project...',
|
|
83
|
+
isSilent: options.output !== 'terminal',
|
|
84
|
+
}).start();
|
|
85
|
+
try {
|
|
86
|
+
// Create analyzer
|
|
87
|
+
const analyzer = new ProjectAnalyzer();
|
|
88
|
+
// Register discoverers based on api-style filter or all by default
|
|
89
|
+
const apiStyles = options.apiStyle ?? ['express', 'nestjs', 'fastify', 'koa'];
|
|
90
|
+
if (apiStyles.includes('express')) {
|
|
91
|
+
analyzer.registerDiscoverer(new ExpressDiscoverer());
|
|
92
|
+
}
|
|
93
|
+
if (apiStyles.includes('nestjs')) {
|
|
94
|
+
analyzer.registerDiscoverer(new NestJSDiscoverer());
|
|
95
|
+
}
|
|
96
|
+
if (apiStyles.includes('fastify')) {
|
|
97
|
+
analyzer.registerDiscoverer(new FastifyDiscoverer());
|
|
98
|
+
}
|
|
99
|
+
if (apiStyles.includes('koa')) {
|
|
100
|
+
analyzer.registerDiscoverer(new KoaDiscoverer());
|
|
101
|
+
}
|
|
102
|
+
// Run analysis
|
|
103
|
+
let result = await analyzer.analyze(absolutePath);
|
|
104
|
+
// Apply rule evaluation
|
|
105
|
+
const ruleEngine = new RuleEngine();
|
|
106
|
+
const findings = ruleEngine.evaluate(result.endpoints);
|
|
107
|
+
result = { ...result, findings };
|
|
108
|
+
// Apply filters
|
|
109
|
+
result = applyFilters(result, options);
|
|
110
|
+
spinner.succeed(`Scan complete`);
|
|
111
|
+
// Format and output
|
|
112
|
+
const formatter = getFormatter(options);
|
|
113
|
+
const output = formatter.format(result);
|
|
114
|
+
if (options.outputFile) {
|
|
115
|
+
fs.writeFileSync(options.outputFile, output, 'utf-8');
|
|
116
|
+
console.log(`Output written to: ${options.outputFile}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(output);
|
|
120
|
+
}
|
|
121
|
+
// Check fail-on condition
|
|
122
|
+
if (options.failOn) {
|
|
123
|
+
const highest = getHighestSeverity(result);
|
|
124
|
+
if (highest && severityOrder[highest] >= severityOrder[options.failOn]) {
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
spinner.fail('Scan failed');
|
|
131
|
+
console.error(error instanceof Error ? error.message : error);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function applyFilters(result, options) {
|
|
136
|
+
let { endpoints, findings } = result;
|
|
137
|
+
// Filter by severity
|
|
138
|
+
if (options.severity) {
|
|
139
|
+
const minSeverity = severityOrder[options.severity];
|
|
140
|
+
findings = findings.filter((f) => severityOrder[f.severity] >= minSeverity);
|
|
141
|
+
}
|
|
142
|
+
// Filter by classification
|
|
143
|
+
if (options.classification && options.classification.length > 0) {
|
|
144
|
+
endpoints = endpoints.filter((e) => options.classification.includes(e.authorization.classification));
|
|
145
|
+
findings = findings.filter((f) => options.classification.includes(f.endpoint.authorization.classification));
|
|
146
|
+
}
|
|
147
|
+
// Filter by method
|
|
148
|
+
if (options.method && options.method.length > 0) {
|
|
149
|
+
endpoints = endpoints.filter((e) => options.method.includes(e.method));
|
|
150
|
+
findings = findings.filter((f) => options.method.includes(f.endpoint.method));
|
|
151
|
+
}
|
|
152
|
+
// Filter by route
|
|
153
|
+
if (options.routeContains) {
|
|
154
|
+
const searchStr = options.routeContains.toLowerCase();
|
|
155
|
+
endpoints = endpoints.filter((e) => e.route.toLowerCase().includes(searchStr));
|
|
156
|
+
findings = findings.filter((f) => f.endpoint.route.toLowerCase().includes(searchStr));
|
|
157
|
+
}
|
|
158
|
+
// Filter by api style
|
|
159
|
+
if (options.apiStyle && options.apiStyle.length > 0) {
|
|
160
|
+
endpoints = endpoints.filter((e) => options.apiStyle.includes(e.type));
|
|
161
|
+
findings = findings.filter((f) => options.apiStyle.includes(f.endpoint.type));
|
|
162
|
+
}
|
|
163
|
+
// Filter by rule
|
|
164
|
+
if (options.rule && options.rule.length > 0) {
|
|
165
|
+
findings = findings.filter((f) => options.rule.includes(f.ruleId));
|
|
166
|
+
}
|
|
167
|
+
// Apply sorting
|
|
168
|
+
if (options.sortBy) {
|
|
169
|
+
const dir = options.sortDir === 'desc' ? -1 : 1;
|
|
170
|
+
endpoints = [...endpoints].sort((a, b) => {
|
|
171
|
+
switch (options.sortBy) {
|
|
172
|
+
case 'route':
|
|
173
|
+
return dir * a.route.localeCompare(b.route);
|
|
174
|
+
case 'method':
|
|
175
|
+
return dir * a.method.localeCompare(b.method);
|
|
176
|
+
case 'classification':
|
|
177
|
+
return dir * a.authorization.classification.localeCompare(b.authorization.classification);
|
|
178
|
+
default:
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
findings = [...findings].sort((a, b) => {
|
|
183
|
+
switch (options.sortBy) {
|
|
184
|
+
case 'severity':
|
|
185
|
+
return dir * (severityOrder[a.severity] - severityOrder[b.severity]);
|
|
186
|
+
case 'route':
|
|
187
|
+
return dir * a.endpoint.route.localeCompare(b.endpoint.route);
|
|
188
|
+
case 'method':
|
|
189
|
+
return dir * a.endpoint.method.localeCompare(b.endpoint.method);
|
|
190
|
+
default:
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return { ...result, endpoints, findings };
|
|
196
|
+
}
|
|
197
|
+
function getFormatter(options) {
|
|
198
|
+
switch (options.output) {
|
|
199
|
+
case 'json':
|
|
200
|
+
return new JsonFormatter();
|
|
201
|
+
case 'markdown':
|
|
202
|
+
return new MarkdownFormatter();
|
|
203
|
+
case 'terminal':
|
|
204
|
+
default:
|
|
205
|
+
return new TerminalFormatter({
|
|
206
|
+
noColor: options.noColor,
|
|
207
|
+
noIcons: options.noIcons,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Severity } from '../core/models/severity.js';
|
|
2
|
+
import { SecurityClassification } from '../core/models/security-classification.js';
|
|
3
|
+
import { HttpMethod } from '../core/models/http-method.js';
|
|
4
|
+
import { EndpointType } from '../core/models/endpoint-type.js';
|
|
5
|
+
export interface ScanOptions {
|
|
6
|
+
output: 'terminal' | 'json' | 'markdown';
|
|
7
|
+
outputFile?: string;
|
|
8
|
+
config?: string;
|
|
9
|
+
severity?: Severity;
|
|
10
|
+
failOn?: Severity;
|
|
11
|
+
sortBy?: 'severity' | 'route' | 'method' | 'classification';
|
|
12
|
+
sortDir?: 'asc' | 'desc';
|
|
13
|
+
classification?: SecurityClassification[];
|
|
14
|
+
method?: HttpMethod[];
|
|
15
|
+
routeContains?: string;
|
|
16
|
+
apiStyle?: EndpointType[];
|
|
17
|
+
rule?: string[];
|
|
18
|
+
groupBy?: string;
|
|
19
|
+
noColor: boolean;
|
|
20
|
+
noIcons: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare const defaultScanOptions: ScanOptions;
|
|
23
|
+
export declare function parseClassificationList(value: string): SecurityClassification[];
|
|
24
|
+
export declare function parseMethodList(value: string): HttpMethod[];
|
|
25
|
+
export declare function parseApiStyleList(value: string): EndpointType[];
|
|
26
|
+
export declare function parseRuleList(value: string): string[];
|
|
27
|
+
//# sourceMappingURL=options.d.ts.map
|