@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
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { createScanCommand } from './cli/commands/scan.js';
|
|
4
|
+
import { createActivateCommand } from './cli/commands/license/activate.js';
|
|
5
|
+
import { createDeactivateCommand } from './cli/commands/license/deactivate.js';
|
|
6
|
+
import { createStatusCommand } from './cli/commands/license/status.js';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('apiposture')
|
|
10
|
+
.description('Static source-code analysis CLI for Node.js API security')
|
|
11
|
+
.version('1.0.0');
|
|
12
|
+
// Add scan command (default)
|
|
13
|
+
program.addCommand(createScanCommand(), { isDefault: true });
|
|
14
|
+
// Add license subcommands
|
|
15
|
+
const licenseCommand = new Command('license')
|
|
16
|
+
.description('Manage license activation');
|
|
17
|
+
licenseCommand.addCommand(createActivateCommand());
|
|
18
|
+
licenseCommand.addCommand(createDeactivateCommand());
|
|
19
|
+
licenseCommand.addCommand(createStatusCommand());
|
|
20
|
+
program.addCommand(licenseCommand);
|
|
21
|
+
// Parse and run
|
|
22
|
+
program.parse();
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { LicenseContext } from '../core/licensing/license-context.js';
|
|
2
|
+
import { LicenseFeature } from '../core/licensing/license-features.js';
|
|
3
|
+
export interface ActivationResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
error?: string;
|
|
6
|
+
licenseType?: string;
|
|
7
|
+
expiresAt?: Date;
|
|
8
|
+
features?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface DeactivationResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LicenseStatus {
|
|
15
|
+
isActive: boolean;
|
|
16
|
+
licenseType?: string;
|
|
17
|
+
expiresAt?: Date;
|
|
18
|
+
features?: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare class LicenseManager {
|
|
21
|
+
private context;
|
|
22
|
+
constructor();
|
|
23
|
+
activate(key: string): Promise<ActivationResult>;
|
|
24
|
+
deactivate(): Promise<DeactivationResult>;
|
|
25
|
+
getStatus(): Promise<LicenseStatus>;
|
|
26
|
+
getContext(): LicenseContext;
|
|
27
|
+
hasFeature(feature: LicenseFeature): boolean;
|
|
28
|
+
private loadContext;
|
|
29
|
+
private loadLicense;
|
|
30
|
+
private saveLicense;
|
|
31
|
+
private removeLicense;
|
|
32
|
+
private isValidKeyFormat;
|
|
33
|
+
private activateWithServer;
|
|
34
|
+
private deactivateWithServer;
|
|
35
|
+
private activateOffline;
|
|
36
|
+
private getMachineId;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=license-manager.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { createCommunityContext, createLicensedContext, } from '../core/licensing/license-context.js';
|
|
6
|
+
import { proFeatures, enterpriseFeatures, } from '../core/licensing/license-features.js';
|
|
7
|
+
const LICENSE_DIR = path.join(os.homedir(), '.apiposture');
|
|
8
|
+
const LICENSE_FILE = path.join(LICENSE_DIR, 'license.json');
|
|
9
|
+
const LICENSE_SERVER = 'https://apiposture.com/api/license';
|
|
10
|
+
const ENV_VAR_KEY = 'APIPOSTURE_LICENSE_KEY';
|
|
11
|
+
export class LicenseManager {
|
|
12
|
+
context;
|
|
13
|
+
constructor() {
|
|
14
|
+
this.context = this.loadContext();
|
|
15
|
+
}
|
|
16
|
+
async activate(key) {
|
|
17
|
+
try {
|
|
18
|
+
// Validate key format
|
|
19
|
+
if (!this.isValidKeyFormat(key)) {
|
|
20
|
+
return { success: false, error: 'Invalid license key format' };
|
|
21
|
+
}
|
|
22
|
+
// Try to activate with server
|
|
23
|
+
const response = await this.activateWithServer(key);
|
|
24
|
+
if (response.success && response.license) {
|
|
25
|
+
// Save license locally
|
|
26
|
+
this.saveLicense(response.license);
|
|
27
|
+
this.context = createLicensedContext(response.license);
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
licenseType: response.license.type,
|
|
31
|
+
expiresAt: response.license.expiresAt ?? undefined,
|
|
32
|
+
features: response.license.features,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { success: false, error: response.error ?? 'Activation failed' };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// For now, allow offline activation for development
|
|
39
|
+
const offlineResult = this.activateOffline(key);
|
|
40
|
+
if (offlineResult.success) {
|
|
41
|
+
return offlineResult;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
error: `Activation failed: ${error instanceof Error ? error.message : error}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async deactivate() {
|
|
50
|
+
try {
|
|
51
|
+
// Try to deactivate with server
|
|
52
|
+
if (this.context.info?.key) {
|
|
53
|
+
await this.deactivateWithServer(this.context.info.key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Continue with local deactivation even if server fails
|
|
58
|
+
}
|
|
59
|
+
// Remove local license
|
|
60
|
+
this.removeLicense();
|
|
61
|
+
this.context = createCommunityContext();
|
|
62
|
+
return { success: true };
|
|
63
|
+
}
|
|
64
|
+
async getStatus() {
|
|
65
|
+
if (!this.context.isLicensed || !this.context.info) {
|
|
66
|
+
return { isActive: false };
|
|
67
|
+
}
|
|
68
|
+
// Check expiration
|
|
69
|
+
if (this.context.info.expiresAt && new Date() > this.context.info.expiresAt) {
|
|
70
|
+
return { isActive: false };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
isActive: true,
|
|
74
|
+
licenseType: this.context.info.type,
|
|
75
|
+
expiresAt: this.context.info.expiresAt ?? undefined,
|
|
76
|
+
features: this.context.info.features,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
getContext() {
|
|
80
|
+
return this.context;
|
|
81
|
+
}
|
|
82
|
+
hasFeature(feature) {
|
|
83
|
+
return this.context.hasFeature(feature);
|
|
84
|
+
}
|
|
85
|
+
loadContext() {
|
|
86
|
+
// Check environment variable first
|
|
87
|
+
const envKey = process.env[ENV_VAR_KEY];
|
|
88
|
+
if (envKey) {
|
|
89
|
+
const offlineResult = this.activateOffline(envKey);
|
|
90
|
+
if (offlineResult.success) {
|
|
91
|
+
return this.context;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Try to load from file
|
|
95
|
+
const license = this.loadLicense();
|
|
96
|
+
if (license) {
|
|
97
|
+
// Check expiration
|
|
98
|
+
if (license.expiresAt && new Date() > license.expiresAt) {
|
|
99
|
+
return createCommunityContext();
|
|
100
|
+
}
|
|
101
|
+
return createLicensedContext(license);
|
|
102
|
+
}
|
|
103
|
+
return createCommunityContext();
|
|
104
|
+
}
|
|
105
|
+
loadLicense() {
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(LICENSE_FILE)) {
|
|
108
|
+
const content = fs.readFileSync(LICENSE_FILE, 'utf-8');
|
|
109
|
+
const data = JSON.parse(content);
|
|
110
|
+
return {
|
|
111
|
+
...data,
|
|
112
|
+
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null,
|
|
113
|
+
activatedAt: new Date(data.activatedAt),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore load errors
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
saveLicense(license) {
|
|
123
|
+
try {
|
|
124
|
+
if (!fs.existsSync(LICENSE_DIR)) {
|
|
125
|
+
fs.mkdirSync(LICENSE_DIR, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
fs.writeFileSync(LICENSE_FILE, JSON.stringify(license, null, 2), 'utf-8');
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.warn('Failed to save license:', error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
removeLicense() {
|
|
134
|
+
try {
|
|
135
|
+
if (fs.existsSync(LICENSE_FILE)) {
|
|
136
|
+
fs.unlinkSync(LICENSE_FILE);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore remove errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
isValidKeyFormat(key) {
|
|
144
|
+
// Expected format: XXXX-XXXX-XXXX-XXXX or similar
|
|
145
|
+
return /^[A-Z0-9]{4}(-[A-Z0-9]{4}){3,}$/i.test(key);
|
|
146
|
+
}
|
|
147
|
+
async activateWithServer(key) {
|
|
148
|
+
const response = await axios.post(`${LICENSE_SERVER}/activate`, { key, machineId: this.getMachineId() }, { timeout: 10000 });
|
|
149
|
+
return response.data;
|
|
150
|
+
}
|
|
151
|
+
async deactivateWithServer(key) {
|
|
152
|
+
await axios.post(`${LICENSE_SERVER}/deactivate`, { key, machineId: this.getMachineId() }, { timeout: 10000 });
|
|
153
|
+
}
|
|
154
|
+
activateOffline(key) {
|
|
155
|
+
// Offline activation for development/testing
|
|
156
|
+
// In production, this would validate against embedded public key
|
|
157
|
+
const keyLower = key.toLowerCase();
|
|
158
|
+
let type = 'pro';
|
|
159
|
+
let features = proFeatures;
|
|
160
|
+
if (keyLower.includes('enterprise') || keyLower.startsWith('ent-')) {
|
|
161
|
+
type = 'enterprise';
|
|
162
|
+
features = enterpriseFeatures;
|
|
163
|
+
}
|
|
164
|
+
const license = {
|
|
165
|
+
key,
|
|
166
|
+
type,
|
|
167
|
+
features,
|
|
168
|
+
expiresAt: null, // Never expires for offline
|
|
169
|
+
activatedAt: new Date(),
|
|
170
|
+
};
|
|
171
|
+
this.saveLicense(license);
|
|
172
|
+
this.context = createLicensedContext(license);
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
licenseType: type,
|
|
176
|
+
features,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
getMachineId() {
|
|
180
|
+
// Simple machine ID based on hostname and platform
|
|
181
|
+
return `${os.hostname()}-${os.platform()}-${os.arch()}`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=license-manager.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Severity } from '../core/models/severity.js';
|
|
2
|
+
import { SecurityClassification } from '../core/models/security-classification.js';
|
|
3
|
+
export interface AccessibilityOptions {
|
|
4
|
+
noColor: boolean;
|
|
5
|
+
noIcons: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare class AccessibilityHelper {
|
|
8
|
+
private options;
|
|
9
|
+
constructor(options?: Partial<AccessibilityOptions>);
|
|
10
|
+
severityIcon(severity: Severity): string;
|
|
11
|
+
severityColor(severity: Severity, text: string): string;
|
|
12
|
+
classificationIcon(classification: SecurityClassification): string;
|
|
13
|
+
bold(text: string): string;
|
|
14
|
+
dim(text: string): string;
|
|
15
|
+
green(text: string): string;
|
|
16
|
+
red(text: string): string;
|
|
17
|
+
yellow(text: string): string;
|
|
18
|
+
cyan(text: string): string;
|
|
19
|
+
checkmark(): string;
|
|
20
|
+
crossmark(): string;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=accessibility-helper.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Severity } from '../core/models/severity.js';
|
|
2
|
+
import { SecurityClassification } from '../core/models/security-classification.js';
|
|
3
|
+
const severityIcons = {
|
|
4
|
+
[Severity.Critical]: '\u26a0\ufe0f',
|
|
5
|
+
[Severity.High]: '\ud83d\udd34',
|
|
6
|
+
[Severity.Medium]: '\ud83d\udfe0',
|
|
7
|
+
[Severity.Low]: '\ud83d\udfe1',
|
|
8
|
+
[Severity.Info]: '\ud83d\udfe2',
|
|
9
|
+
};
|
|
10
|
+
const severityColors = {
|
|
11
|
+
[Severity.Critical]: '\x1b[91m', // bright red
|
|
12
|
+
[Severity.High]: '\x1b[31m', // red
|
|
13
|
+
[Severity.Medium]: '\x1b[33m', // yellow
|
|
14
|
+
[Severity.Low]: '\x1b[36m', // cyan
|
|
15
|
+
[Severity.Info]: '\x1b[32m', // green
|
|
16
|
+
};
|
|
17
|
+
const classificationIcons = {
|
|
18
|
+
[SecurityClassification.Public]: '\ud83c\udf10',
|
|
19
|
+
[SecurityClassification.Authenticated]: '\ud83d\udd10',
|
|
20
|
+
[SecurityClassification.RoleRestricted]: '\ud83d\udc65',
|
|
21
|
+
[SecurityClassification.PolicyRestricted]: '\ud83d\udcdc',
|
|
22
|
+
};
|
|
23
|
+
export class AccessibilityHelper {
|
|
24
|
+
options;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = {
|
|
27
|
+
noColor: options.noColor ?? false,
|
|
28
|
+
noIcons: options.noIcons ?? false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
severityIcon(severity) {
|
|
32
|
+
if (this.options.noIcons) {
|
|
33
|
+
return `[${severity.toUpperCase()}]`;
|
|
34
|
+
}
|
|
35
|
+
return severityIcons[severity];
|
|
36
|
+
}
|
|
37
|
+
severityColor(severity, text) {
|
|
38
|
+
if (this.options.noColor) {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
return `${severityColors[severity]}${text}\x1b[0m`;
|
|
42
|
+
}
|
|
43
|
+
classificationIcon(classification) {
|
|
44
|
+
if (this.options.noIcons) {
|
|
45
|
+
return `[${classification}]`;
|
|
46
|
+
}
|
|
47
|
+
return classificationIcons[classification];
|
|
48
|
+
}
|
|
49
|
+
bold(text) {
|
|
50
|
+
if (this.options.noColor) {
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
return `\x1b[1m${text}\x1b[0m`;
|
|
54
|
+
}
|
|
55
|
+
dim(text) {
|
|
56
|
+
if (this.options.noColor) {
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
return `\x1b[2m${text}\x1b[0m`;
|
|
60
|
+
}
|
|
61
|
+
green(text) {
|
|
62
|
+
if (this.options.noColor) {
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
return `\x1b[32m${text}\x1b[0m`;
|
|
66
|
+
}
|
|
67
|
+
red(text) {
|
|
68
|
+
if (this.options.noColor) {
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
return `\x1b[31m${text}\x1b[0m`;
|
|
72
|
+
}
|
|
73
|
+
yellow(text) {
|
|
74
|
+
if (this.options.noColor) {
|
|
75
|
+
return text;
|
|
76
|
+
}
|
|
77
|
+
return `\x1b[33m${text}\x1b[0m`;
|
|
78
|
+
}
|
|
79
|
+
cyan(text) {
|
|
80
|
+
if (this.options.noColor) {
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
return `\x1b[36m${text}\x1b[0m`;
|
|
84
|
+
}
|
|
85
|
+
checkmark() {
|
|
86
|
+
if (this.options.noIcons) {
|
|
87
|
+
return '[OK]';
|
|
88
|
+
}
|
|
89
|
+
return '\u2714';
|
|
90
|
+
}
|
|
91
|
+
crossmark() {
|
|
92
|
+
if (this.options.noIcons) {
|
|
93
|
+
return '[X]';
|
|
94
|
+
}
|
|
95
|
+
return '\u2716';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=accessibility-helper.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ScanResult } from '../core/models/scan-result.js';
|
|
2
|
+
export interface OutputFormatter {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
format(result: ScanResult): string;
|
|
5
|
+
}
|
|
6
|
+
export interface FormatterOptions {
|
|
7
|
+
noColor?: boolean;
|
|
8
|
+
noIcons?: boolean;
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=formatter-interface.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OutputFormatter } from './formatter-interface.js';
|
|
2
|
+
import { ScanResult } from '../core/models/scan-result.js';
|
|
3
|
+
export declare class JsonFormatter implements OutputFormatter {
|
|
4
|
+
readonly name = "json";
|
|
5
|
+
format(result: ScanResult): string;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=json-formatter.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getScanSummary } from '../core/models/scan-result.js';
|
|
2
|
+
export class JsonFormatter {
|
|
3
|
+
name = 'json';
|
|
4
|
+
format(result) {
|
|
5
|
+
const summary = getScanSummary(result);
|
|
6
|
+
const output = {
|
|
7
|
+
scanInfo: {
|
|
8
|
+
projectPath: result.projectPath,
|
|
9
|
+
scanDate: result.scanDate.toISOString(),
|
|
10
|
+
filesScanned: result.filesScanned,
|
|
11
|
+
scanDurationMs: result.scanDurationMs,
|
|
12
|
+
},
|
|
13
|
+
summary: {
|
|
14
|
+
totalEndpoints: summary.totalEndpoints,
|
|
15
|
+
totalFindings: summary.totalFindings,
|
|
16
|
+
findingsBySeverity: summary.findingsBySeverity,
|
|
17
|
+
suppressedFindings: summary.suppressedFindings,
|
|
18
|
+
},
|
|
19
|
+
endpoints: result.endpoints.map((e) => ({
|
|
20
|
+
route: e.route,
|
|
21
|
+
method: e.method,
|
|
22
|
+
handler: e.handlerName,
|
|
23
|
+
controller: e.controllerName,
|
|
24
|
+
framework: e.type,
|
|
25
|
+
location: {
|
|
26
|
+
file: e.location.filePath,
|
|
27
|
+
line: e.location.line,
|
|
28
|
+
column: e.location.column,
|
|
29
|
+
},
|
|
30
|
+
authorization: {
|
|
31
|
+
classification: e.authorization.classification,
|
|
32
|
+
isAuthenticated: e.authorization.isAuthenticated,
|
|
33
|
+
isExplicitlyPublic: e.authorization.isExplicitlyPublic,
|
|
34
|
+
roles: e.authorization.roles,
|
|
35
|
+
policies: e.authorization.policies,
|
|
36
|
+
},
|
|
37
|
+
})),
|
|
38
|
+
findings: result.findings
|
|
39
|
+
.filter((f) => !f.suppressed)
|
|
40
|
+
.map((f) => ({
|
|
41
|
+
ruleId: f.ruleId,
|
|
42
|
+
ruleName: f.ruleName,
|
|
43
|
+
severity: f.severity,
|
|
44
|
+
message: f.message,
|
|
45
|
+
endpoint: {
|
|
46
|
+
route: f.endpoint.route,
|
|
47
|
+
method: f.endpoint.method,
|
|
48
|
+
},
|
|
49
|
+
location: {
|
|
50
|
+
file: f.location.filePath,
|
|
51
|
+
line: f.location.line,
|
|
52
|
+
column: f.location.column,
|
|
53
|
+
},
|
|
54
|
+
recommendation: f.recommendation,
|
|
55
|
+
})),
|
|
56
|
+
suppressedFindings: result.findings
|
|
57
|
+
.filter((f) => f.suppressed)
|
|
58
|
+
.map((f) => ({
|
|
59
|
+
ruleId: f.ruleId,
|
|
60
|
+
ruleName: f.ruleName,
|
|
61
|
+
severity: f.severity,
|
|
62
|
+
endpoint: {
|
|
63
|
+
route: f.endpoint.route,
|
|
64
|
+
method: f.endpoint.method,
|
|
65
|
+
},
|
|
66
|
+
suppressionReason: f.suppressionReason,
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
return JSON.stringify(output, null, 2);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=json-formatter.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { OutputFormatter } from './formatter-interface.js';
|
|
2
|
+
import { ScanResult } from '../core/models/scan-result.js';
|
|
3
|
+
export declare class MarkdownFormatter implements OutputFormatter {
|
|
4
|
+
readonly name = "markdown";
|
|
5
|
+
format(result: ScanResult): string;
|
|
6
|
+
private formatFinding;
|
|
7
|
+
private getSeverityBadge;
|
|
8
|
+
private sortFindingsBySeverity;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=markdown-formatter.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getScanSummary } from '../core/models/scan-result.js';
|
|
2
|
+
import { Severity, severityOrder } from '../core/models/severity.js';
|
|
3
|
+
import { formatSourceLocation } from '../core/models/source-location.js';
|
|
4
|
+
export class MarkdownFormatter {
|
|
5
|
+
name = 'markdown';
|
|
6
|
+
format(result) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
const summary = getScanSummary(result);
|
|
9
|
+
// Title
|
|
10
|
+
lines.push('# ApiPosture Security Scan Report');
|
|
11
|
+
lines.push('');
|
|
12
|
+
// Scan Info
|
|
13
|
+
lines.push('## Scan Information');
|
|
14
|
+
lines.push('');
|
|
15
|
+
lines.push(`| Property | Value |`);
|
|
16
|
+
lines.push(`|----------|-------|`);
|
|
17
|
+
lines.push(`| Project | \`${result.projectPath}\` |`);
|
|
18
|
+
lines.push(`| Scan Date | ${result.scanDate.toISOString()} |`);
|
|
19
|
+
lines.push(`| Files Scanned | ${result.filesScanned} |`);
|
|
20
|
+
lines.push(`| Endpoints Found | ${summary.totalEndpoints} |`);
|
|
21
|
+
lines.push(`| Scan Duration | ${result.scanDurationMs}ms |`);
|
|
22
|
+
lines.push('');
|
|
23
|
+
// Summary
|
|
24
|
+
lines.push('## Summary');
|
|
25
|
+
lines.push('');
|
|
26
|
+
if (summary.totalFindings === 0) {
|
|
27
|
+
lines.push('**No security findings detected.**');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
lines.push(`**Total Findings:** ${summary.totalFindings}`);
|
|
31
|
+
lines.push('');
|
|
32
|
+
lines.push('| Severity | Count |');
|
|
33
|
+
lines.push('|----------|-------|');
|
|
34
|
+
for (const severity of Object.values(Severity).reverse()) {
|
|
35
|
+
const count = summary.findingsBySeverity[severity];
|
|
36
|
+
if (count > 0) {
|
|
37
|
+
const badge = this.getSeverityBadge(severity);
|
|
38
|
+
lines.push(`| ${badge} ${severity} | ${count} |`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
lines.push('');
|
|
43
|
+
// Findings
|
|
44
|
+
if (summary.totalFindings > 0) {
|
|
45
|
+
lines.push('## Findings');
|
|
46
|
+
lines.push('');
|
|
47
|
+
const activeFindings = result.findings.filter((f) => !f.suppressed);
|
|
48
|
+
const sortedFindings = this.sortFindingsBySeverity(activeFindings);
|
|
49
|
+
for (const finding of sortedFindings) {
|
|
50
|
+
lines.push(this.formatFinding(finding));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Endpoints Table
|
|
54
|
+
lines.push('## Endpoints');
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('| Method | Route | Classification | Framework | Location |');
|
|
57
|
+
lines.push('|--------|-------|----------------|-----------|----------|');
|
|
58
|
+
for (const endpoint of result.endpoints) {
|
|
59
|
+
lines.push(`| ${endpoint.method} | \`${endpoint.route}\` | ${endpoint.authorization.classification} | ${endpoint.type} | ${endpoint.location.filePath}:${endpoint.location.line} |`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
// Suppressed
|
|
63
|
+
if (summary.suppressedFindings > 0) {
|
|
64
|
+
lines.push('## Suppressed Findings');
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push(`${summary.suppressedFindings} findings were suppressed.`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
}
|
|
69
|
+
// Footer
|
|
70
|
+
lines.push('---');
|
|
71
|
+
lines.push('*Generated by ApiPosture*');
|
|
72
|
+
lines.push('');
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
formatFinding(finding) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
const badge = this.getSeverityBadge(finding.severity);
|
|
78
|
+
lines.push(`### ${badge} ${finding.ruleId}: ${finding.ruleName}`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push(`**Severity:** ${finding.severity}`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(`**Endpoint:** \`${finding.endpoint.method} ${finding.endpoint.route}\``);
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push(`**Location:** \`${formatSourceLocation(finding.location)}\``);
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push(`**Message:** ${finding.message}`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push('**Recommendation:**');
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push(`> ${finding.recommendation}`);
|
|
91
|
+
lines.push('');
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
getSeverityBadge(severity) {
|
|
95
|
+
switch (severity) {
|
|
96
|
+
case Severity.Critical:
|
|
97
|
+
return '\ud83d\udfe5';
|
|
98
|
+
case Severity.High:
|
|
99
|
+
return '\ud83d\udd34';
|
|
100
|
+
case Severity.Medium:
|
|
101
|
+
return '\ud83d\udfe0';
|
|
102
|
+
case Severity.Low:
|
|
103
|
+
return '\ud83d\udfe1';
|
|
104
|
+
case Severity.Info:
|
|
105
|
+
return '\ud83d\udfe2';
|
|
106
|
+
default:
|
|
107
|
+
return '\u26aa';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
sortFindingsBySeverity(findings) {
|
|
111
|
+
return [...findings].sort((a, b) => severityOrder[b.severity] - severityOrder[a.severity]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=markdown-formatter.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OutputFormatter, FormatterOptions } from './formatter-interface.js';
|
|
2
|
+
import { ScanResult } from '../core/models/scan-result.js';
|
|
3
|
+
export declare class TerminalFormatter implements OutputFormatter {
|
|
4
|
+
readonly name = "terminal";
|
|
5
|
+
private helper;
|
|
6
|
+
constructor(options?: FormatterOptions);
|
|
7
|
+
format(result: ScanResult): string;
|
|
8
|
+
private formatFindingsSummary;
|
|
9
|
+
private formatFinding;
|
|
10
|
+
private sortFindingsBySeverity;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=terminal-formatter.d.ts.map
|