@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.
Files changed (110) hide show
  1. package/.apiposture.json.example +56 -0
  2. package/.github/workflows/publish.yml +38 -0
  3. package/.github/workflows/test.yml +42 -0
  4. package/LICENSE +21 -0
  5. package/README.md +156 -0
  6. package/dist/cli/commands/license/activate.d.ts +3 -0
  7. package/dist/cli/commands/license/activate.js +35 -0
  8. package/dist/cli/commands/license/deactivate.d.ts +3 -0
  9. package/dist/cli/commands/license/deactivate.js +28 -0
  10. package/dist/cli/commands/license/status.d.ts +3 -0
  11. package/dist/cli/commands/license/status.js +36 -0
  12. package/dist/cli/commands/scan.d.ts +3 -0
  13. package/dist/cli/commands/scan.js +211 -0
  14. package/dist/cli/options.d.ts +27 -0
  15. package/dist/cli/options.js +30 -0
  16. package/dist/core/analysis/project-analyzer.d.ts +16 -0
  17. package/dist/core/analysis/project-analyzer.js +54 -0
  18. package/dist/core/analysis/source-file-loader.d.ts +32 -0
  19. package/dist/core/analysis/source-file-loader.js +155 -0
  20. package/dist/core/authorization/authorization-extractor.d.ts +11 -0
  21. package/dist/core/authorization/authorization-extractor.js +2 -0
  22. package/dist/core/authorization/express-auth-extractor.d.ts +10 -0
  23. package/dist/core/authorization/express-auth-extractor.js +106 -0
  24. package/dist/core/authorization/global-auth-analyzer.d.ts +12 -0
  25. package/dist/core/authorization/global-auth-analyzer.js +74 -0
  26. package/dist/core/authorization/nestjs-auth-extractor.d.ts +13 -0
  27. package/dist/core/authorization/nestjs-auth-extractor.js +142 -0
  28. package/dist/core/configuration/config-loader.d.ts +27 -0
  29. package/dist/core/configuration/config-loader.js +72 -0
  30. package/dist/core/configuration/suppression-matcher.d.ts +14 -0
  31. package/dist/core/configuration/suppression-matcher.js +79 -0
  32. package/dist/core/discovery/discoverer-interface.d.ts +7 -0
  33. package/dist/core/discovery/discoverer-interface.js +2 -0
  34. package/dist/core/discovery/express-discoverer.d.ts +20 -0
  35. package/dist/core/discovery/express-discoverer.js +223 -0
  36. package/dist/core/discovery/fastify-discoverer.d.ts +19 -0
  37. package/dist/core/discovery/fastify-discoverer.js +249 -0
  38. package/dist/core/discovery/framework-detector.d.ts +9 -0
  39. package/dist/core/discovery/framework-detector.js +61 -0
  40. package/dist/core/discovery/index.d.ts +8 -0
  41. package/dist/core/discovery/index.js +8 -0
  42. package/dist/core/discovery/koa-discoverer.d.ts +16 -0
  43. package/dist/core/discovery/koa-discoverer.js +151 -0
  44. package/dist/core/discovery/nestjs-discoverer.d.ts +16 -0
  45. package/dist/core/discovery/nestjs-discoverer.js +180 -0
  46. package/dist/core/discovery/route-group-registry.d.ts +18 -0
  47. package/dist/core/discovery/route-group-registry.js +50 -0
  48. package/dist/core/licensing/license-context.d.ts +17 -0
  49. package/dist/core/licensing/license-context.js +15 -0
  50. package/dist/core/licensing/license-features.d.ts +14 -0
  51. package/dist/core/licensing/license-features.js +47 -0
  52. package/dist/core/models/authorization-info.d.ts +13 -0
  53. package/dist/core/models/authorization-info.js +25 -0
  54. package/dist/core/models/endpoint-type.d.ts +8 -0
  55. package/dist/core/models/endpoint-type.js +12 -0
  56. package/dist/core/models/endpoint.d.ts +16 -0
  57. package/dist/core/models/endpoint.js +16 -0
  58. package/dist/core/models/finding.d.ts +19 -0
  59. package/dist/core/models/finding.js +8 -0
  60. package/dist/core/models/http-method.d.ts +14 -0
  61. package/dist/core/models/http-method.js +25 -0
  62. package/dist/core/models/index.d.ts +10 -0
  63. package/dist/core/models/index.js +10 -0
  64. package/dist/core/models/scan-result.d.ts +21 -0
  65. package/dist/core/models/scan-result.js +35 -0
  66. package/dist/core/models/security-classification.d.ts +8 -0
  67. package/dist/core/models/security-classification.js +12 -0
  68. package/dist/core/models/severity.d.ts +11 -0
  69. package/dist/core/models/severity.js +23 -0
  70. package/dist/core/models/source-location.d.ts +7 -0
  71. package/dist/core/models/source-location.js +4 -0
  72. package/dist/index.d.ts +3 -0
  73. package/dist/index.js +23 -0
  74. package/dist/licensing/license-manager.d.ts +38 -0
  75. package/dist/licensing/license-manager.js +184 -0
  76. package/dist/output/accessibility-helper.d.ts +22 -0
  77. package/dist/output/accessibility-helper.js +98 -0
  78. package/dist/output/formatter-interface.d.ts +11 -0
  79. package/dist/output/formatter-interface.js +2 -0
  80. package/dist/output/index.d.ts +6 -0
  81. package/dist/output/index.js +6 -0
  82. package/dist/output/json-formatter.d.ts +7 -0
  83. package/dist/output/json-formatter.js +72 -0
  84. package/dist/output/markdown-formatter.d.ts +10 -0
  85. package/dist/output/markdown-formatter.js +114 -0
  86. package/dist/output/terminal-formatter.d.ts +12 -0
  87. package/dist/output/terminal-formatter.js +82 -0
  88. package/dist/rules/consistency/controller-action-conflict.d.ts +19 -0
  89. package/dist/rules/consistency/controller-action-conflict.js +40 -0
  90. package/dist/rules/consistency/missing-auth-on-writes.d.ts +21 -0
  91. package/dist/rules/consistency/missing-auth-on-writes.js +59 -0
  92. package/dist/rules/exposure/allow-anonymous-on-write.d.ts +20 -0
  93. package/dist/rules/exposure/allow-anonymous-on-write.js +42 -0
  94. package/dist/rules/exposure/public-without-explicit-intent.d.ts +20 -0
  95. package/dist/rules/exposure/public-without-explicit-intent.js +58 -0
  96. package/dist/rules/index.d.ts +11 -0
  97. package/dist/rules/index.js +11 -0
  98. package/dist/rules/privilege/excessive-role-access.d.ts +20 -0
  99. package/dist/rules/privilege/excessive-role-access.js +36 -0
  100. package/dist/rules/privilege/weak-role-naming.d.ts +20 -0
  101. package/dist/rules/privilege/weak-role-naming.js +50 -0
  102. package/dist/rules/rule-engine.d.ts +15 -0
  103. package/dist/rules/rule-engine.js +52 -0
  104. package/dist/rules/rule-interface.d.ts +16 -0
  105. package/dist/rules/rule-interface.js +2 -0
  106. package/dist/rules/surface/sensitive-route-keywords.d.ts +20 -0
  107. package/dist/rules/surface/sensitive-route-keywords.js +63 -0
  108. package/dist/rules/surface/unprotected-endpoint.d.ts +20 -0
  109. package/dist/rules/surface/unprotected-endpoint.js +61 -0
  110. 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,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createActivateCommand(): Command;
3
+ //# sourceMappingURL=activate.d.ts.map
@@ -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,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createDeactivateCommand(): Command;
3
+ //# sourceMappingURL=deactivate.d.ts.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,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createStatusCommand(): Command;
3
+ //# sourceMappingURL=status.d.ts.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,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createScanCommand(): Command;
3
+ //# sourceMappingURL=scan.d.ts.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