@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,7 @@
1
+ export interface SourceLocation {
2
+ filePath: string;
3
+ line: number;
4
+ column: number;
5
+ }
6
+ export declare function formatSourceLocation(location: SourceLocation): string;
7
+ //# sourceMappingURL=source-location.d.ts.map
@@ -0,0 +1,4 @@
1
+ export function formatSourceLocation(location) {
2
+ return `${location.filePath}:${location.line}:${location.column}`;
3
+ }
4
+ //# sourceMappingURL=source-location.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=formatter-interface.js.map
@@ -0,0 +1,6 @@
1
+ export * from './formatter-interface.js';
2
+ export * from './accessibility-helper.js';
3
+ export * from './terminal-formatter.js';
4
+ export * from './json-formatter.js';
5
+ export * from './markdown-formatter.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export * from './formatter-interface.js';
2
+ export * from './accessibility-helper.js';
3
+ export * from './terminal-formatter.js';
4
+ export * from './json-formatter.js';
5
+ export * from './markdown-formatter.js';
6
+ //# sourceMappingURL=index.js.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