@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,82 @@
1
+ import { AccessibilityHelper } from './accessibility-helper.js';
2
+ import { getScanSummary } from '../core/models/scan-result.js';
3
+ import { Severity, severityOrder } from '../core/models/severity.js';
4
+ import { formatSourceLocation } from '../core/models/source-location.js';
5
+ export class TerminalFormatter {
6
+ name = 'terminal';
7
+ helper;
8
+ constructor(options = {}) {
9
+ this.helper = new AccessibilityHelper({
10
+ noColor: options.noColor,
11
+ noIcons: options.noIcons,
12
+ });
13
+ }
14
+ format(result) {
15
+ const lines = [];
16
+ const summary = getScanSummary(result);
17
+ // Header
18
+ lines.push('');
19
+ lines.push(this.helper.bold('ApiPosture Security Scan Results'));
20
+ lines.push(this.helper.dim('='.repeat(50)));
21
+ lines.push('');
22
+ // Scan info
23
+ lines.push(`Project: ${this.helper.cyan(result.projectPath)}`);
24
+ lines.push(`Files scanned: ${result.filesScanned}`);
25
+ lines.push(`Endpoints found: ${summary.totalEndpoints}`);
26
+ lines.push(`Scan duration: ${result.scanDurationMs}ms`);
27
+ lines.push('');
28
+ // Findings summary
29
+ if (summary.totalFindings === 0) {
30
+ lines.push(this.helper.green(`${this.helper.checkmark()} No security findings detected!`));
31
+ }
32
+ else {
33
+ lines.push(this.helper.bold('Findings Summary:'));
34
+ lines.push(this.formatFindingsSummary(summary.findingsBySeverity));
35
+ lines.push('');
36
+ // Findings details
37
+ lines.push(this.helper.bold('Findings Details:'));
38
+ lines.push(this.helper.dim('-'.repeat(50)));
39
+ const activeFindings = result.findings.filter((f) => !f.suppressed);
40
+ const sortedFindings = this.sortFindingsBySeverity(activeFindings);
41
+ for (const finding of sortedFindings) {
42
+ lines.push(this.formatFinding(finding));
43
+ }
44
+ }
45
+ // Suppressed findings
46
+ if (summary.suppressedFindings > 0) {
47
+ lines.push('');
48
+ lines.push(this.helper.dim(`(${summary.suppressedFindings} findings suppressed)`));
49
+ }
50
+ lines.push('');
51
+ return lines.join('\n');
52
+ }
53
+ formatFindingsSummary(bySeverity) {
54
+ const parts = [];
55
+ for (const severity of Object.values(Severity).reverse()) {
56
+ const count = bySeverity[severity];
57
+ if (count > 0) {
58
+ const icon = this.helper.severityIcon(severity);
59
+ const text = this.helper.severityColor(severity, `${severity}: ${count}`);
60
+ parts.push(` ${icon} ${text}`);
61
+ }
62
+ }
63
+ return parts.join('\n');
64
+ }
65
+ formatFinding(finding) {
66
+ const lines = [];
67
+ const icon = this.helper.severityIcon(finding.severity);
68
+ const severityText = this.helper.severityColor(finding.severity, finding.severity.toUpperCase());
69
+ lines.push('');
70
+ lines.push(`${icon} ${this.helper.bold(finding.ruleId)}: ${finding.ruleName}`);
71
+ lines.push(` Severity: ${severityText}`);
72
+ lines.push(` Endpoint: ${this.helper.cyan(`${finding.endpoint.method} ${finding.endpoint.route}`)}`);
73
+ lines.push(` Location: ${this.helper.dim(formatSourceLocation(finding.location))}`);
74
+ lines.push(` Message: ${finding.message}`);
75
+ lines.push(` ${this.helper.dim('Recommendation:')} ${finding.recommendation}`);
76
+ return lines.join('\n');
77
+ }
78
+ sortFindingsBySeverity(findings) {
79
+ return [...findings].sort((a, b) => severityOrder[b.severity] - severityOrder[a.severity]);
80
+ }
81
+ }
82
+ //# sourceMappingURL=terminal-formatter.js.map
@@ -0,0 +1,19 @@
1
+ import { Endpoint } from '../../core/models/endpoint.js';
2
+ import { Finding } from '../../core/models/finding.js';
3
+ import { Severity } from '../../core/models/severity.js';
4
+ import { SecurityRule } from '../rule-interface.js';
5
+ /**
6
+ * AP003: Controller/action authorization conflict
7
+ *
8
+ * Detects when a method-level @Public decorator overrides class-level
9
+ * @UseGuards. This can indicate a security misconfiguration where
10
+ * developers may not realize the method is bypassing controller auth.
11
+ */
12
+ export declare class ControllerActionConflict implements SecurityRule {
13
+ readonly id = "AP003";
14
+ readonly name = "Controller/action authorization conflict";
15
+ readonly description = "Method-level public marker overrides class-level authentication guards";
16
+ readonly severity = Severity.Medium;
17
+ evaluate(endpoint: Endpoint): Finding[];
18
+ }
19
+ //# sourceMappingURL=controller-action-conflict.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { createFinding } from '../../core/models/finding.js';
2
+ import { Severity } from '../../core/models/severity.js';
3
+ import { EndpointType } from '../../core/models/endpoint-type.js';
4
+ /**
5
+ * AP003: Controller/action authorization conflict
6
+ *
7
+ * Detects when a method-level @Public decorator overrides class-level
8
+ * @UseGuards. This can indicate a security misconfiguration where
9
+ * developers may not realize the method is bypassing controller auth.
10
+ */
11
+ export class ControllerActionConflict {
12
+ id = 'AP003';
13
+ name = 'Controller/action authorization conflict';
14
+ description = 'Method-level public marker overrides class-level authentication guards';
15
+ severity = Severity.Medium;
16
+ evaluate(endpoint) {
17
+ const findings = [];
18
+ // This rule primarily applies to NestJS decorator patterns
19
+ if (endpoint.type !== EndpointType.NestJS) {
20
+ return findings;
21
+ }
22
+ const auth = endpoint.authorization;
23
+ // Detect conflict: both authenticated (from class) and explicitly public (from method)
24
+ if (auth.isAuthenticated && auth.isExplicitlyPublic) {
25
+ findings.push(createFinding({
26
+ ruleId: this.id,
27
+ ruleName: this.name,
28
+ severity: this.severity,
29
+ message: `${endpoint.method} ${endpoint.route} has @Public overriding class-level guards`,
30
+ endpoint,
31
+ location: endpoint.location,
32
+ recommendation: 'This endpoint has @Public decorator that overrides class-level @UseGuards. ' +
33
+ 'Ensure this is intentional. If the endpoint should be public, consider ' +
34
+ 'documenting why. If not, remove the @Public decorator.',
35
+ }));
36
+ }
37
+ return findings;
38
+ }
39
+ }
40
+ //# sourceMappingURL=controller-action-conflict.js.map
@@ -0,0 +1,21 @@
1
+ import { Endpoint } from '../../core/models/endpoint.js';
2
+ import { Finding } from '../../core/models/finding.js';
3
+ import { Severity } from '../../core/models/severity.js';
4
+ import { SecurityRule } from '../rule-interface.js';
5
+ /**
6
+ * AP004: Missing authentication on write operations
7
+ *
8
+ * Detects write operations (POST, PUT, DELETE, PATCH) that have no
9
+ * authentication and no explicit public marker. This is the most
10
+ * dangerous scenario: an unprotected write endpoint that may have
11
+ * been accidentally left open.
12
+ */
13
+ export declare class MissingAuthOnWrites implements SecurityRule {
14
+ readonly id = "AP004";
15
+ readonly name = "Missing authentication on write operations";
16
+ readonly description = "Write operation has no authentication and no explicit public marker";
17
+ readonly severity = Severity.Critical;
18
+ evaluate(endpoint: Endpoint): Finding[];
19
+ private getRecommendation;
20
+ }
21
+ //# sourceMappingURL=missing-auth-on-writes.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { createFinding } from '../../core/models/finding.js';
2
+ import { Severity } from '../../core/models/severity.js';
3
+ import { SecurityClassification } from '../../core/models/security-classification.js';
4
+ import { isWriteMethod } from '../../core/models/http-method.js';
5
+ /**
6
+ * AP004: Missing authentication on write operations
7
+ *
8
+ * Detects write operations (POST, PUT, DELETE, PATCH) that have no
9
+ * authentication and no explicit public marker. This is the most
10
+ * dangerous scenario: an unprotected write endpoint that may have
11
+ * been accidentally left open.
12
+ */
13
+ export class MissingAuthOnWrites {
14
+ id = 'AP004';
15
+ name = 'Missing authentication on write operations';
16
+ description = 'Write operation has no authentication and no explicit public marker';
17
+ severity = Severity.Critical;
18
+ evaluate(endpoint) {
19
+ const findings = [];
20
+ const auth = endpoint.authorization;
21
+ // Flag if:
22
+ // 1. It's a write method
23
+ // 2. No authentication
24
+ // 3. No explicit public marker (so it's accidentally open, not intentionally)
25
+ if (isWriteMethod(endpoint.method) &&
26
+ auth.classification === SecurityClassification.Public &&
27
+ !auth.isAuthenticated &&
28
+ !auth.isExplicitlyPublic) {
29
+ findings.push(createFinding({
30
+ ruleId: this.id,
31
+ ruleName: this.name,
32
+ severity: this.severity,
33
+ message: `${endpoint.method} ${endpoint.route} is an unprotected write operation`,
34
+ endpoint,
35
+ location: endpoint.location,
36
+ recommendation: this.getRecommendation(endpoint),
37
+ }));
38
+ }
39
+ return findings;
40
+ }
41
+ getRecommendation(endpoint) {
42
+ switch (endpoint.type) {
43
+ case 'express':
44
+ return ('CRITICAL: Add authentication middleware immediately. ' +
45
+ 'Example: router.post("/path", passport.authenticate("jwt"), handler)');
46
+ case 'nestjs':
47
+ return ('CRITICAL: Add @UseGuards(AuthGuard) decorator immediately. ' +
48
+ 'If this endpoint must be public, add @Public() to make intent explicit.');
49
+ case 'fastify':
50
+ return ('CRITICAL: Add authentication via preHandler hook. ' +
51
+ 'Example: { preHandler: [fastify.authenticate] }');
52
+ case 'koa':
53
+ return 'CRITICAL: Add authentication middleware before the route handler.';
54
+ default:
55
+ return 'CRITICAL: Add authentication to this write endpoint immediately.';
56
+ }
57
+ }
58
+ }
59
+ //# sourceMappingURL=missing-auth-on-writes.js.map
@@ -0,0 +1,20 @@
1
+ import { Endpoint } from '../../core/models/endpoint.js';
2
+ import { Finding } from '../../core/models/finding.js';
3
+ import { Severity } from '../../core/models/severity.js';
4
+ import { SecurityRule } from '../rule-interface.js';
5
+ /**
6
+ * AP002: AllowAnonymous on write operation
7
+ *
8
+ * Detects write operations (POST, PUT, DELETE, PATCH) that have been
9
+ * explicitly marked as public. Write operations should typically require
10
+ * authentication to prevent unauthorized data modification.
11
+ */
12
+ export declare class AllowAnonymousOnWrite implements SecurityRule {
13
+ readonly id = "AP002";
14
+ readonly name = "AllowAnonymous on write operation";
15
+ readonly description = "Write operation (POST/PUT/DELETE/PATCH) is explicitly marked as public";
16
+ readonly severity = Severity.High;
17
+ evaluate(endpoint: Endpoint): Finding[];
18
+ private getRecommendation;
19
+ }
20
+ //# sourceMappingURL=allow-anonymous-on-write.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { createFinding } from '../../core/models/finding.js';
2
+ import { Severity } from '../../core/models/severity.js';
3
+ import { isWriteMethod } from '../../core/models/http-method.js';
4
+ /**
5
+ * AP002: AllowAnonymous on write operation
6
+ *
7
+ * Detects write operations (POST, PUT, DELETE, PATCH) that have been
8
+ * explicitly marked as public. Write operations should typically require
9
+ * authentication to prevent unauthorized data modification.
10
+ */
11
+ export class AllowAnonymousOnWrite {
12
+ id = 'AP002';
13
+ name = 'AllowAnonymous on write operation';
14
+ description = 'Write operation (POST/PUT/DELETE/PATCH) is explicitly marked as public';
15
+ severity = Severity.High;
16
+ evaluate(endpoint) {
17
+ const findings = [];
18
+ const auth = endpoint.authorization;
19
+ // Only flag if:
20
+ // 1. It's a write method
21
+ // 2. It's explicitly marked as public (intentional)
22
+ if (isWriteMethod(endpoint.method) && auth.isExplicitlyPublic) {
23
+ findings.push(createFinding({
24
+ ruleId: this.id,
25
+ ruleName: this.name,
26
+ severity: this.severity,
27
+ message: `${endpoint.method} ${endpoint.route} is a write operation explicitly marked as public`,
28
+ endpoint,
29
+ location: endpoint.location,
30
+ recommendation: this.getRecommendation(endpoint),
31
+ }));
32
+ }
33
+ return findings;
34
+ }
35
+ getRecommendation(endpoint) {
36
+ return (`Write operations like ${endpoint.method} typically require authentication. ` +
37
+ 'If public access is truly needed (e.g., user registration, contact form), ' +
38
+ 'consider adding rate limiting and input validation. ' +
39
+ 'Otherwise, remove the public marker and add authentication.');
40
+ }
41
+ }
42
+ //# sourceMappingURL=allow-anonymous-on-write.js.map
@@ -0,0 +1,20 @@
1
+ import { Endpoint } from '../../core/models/endpoint.js';
2
+ import { Finding } from '../../core/models/finding.js';
3
+ import { Severity } from '../../core/models/severity.js';
4
+ import { SecurityRule } from '../rule-interface.js';
5
+ /**
6
+ * AP001: Public without explicit intent
7
+ *
8
+ * Detects endpoints that are publicly accessible without an explicit
9
+ * @Public decorator or allowAnonymous middleware. This catches endpoints
10
+ * that may be accidentally exposed due to missing authentication.
11
+ */
12
+ export declare class PublicWithoutExplicitIntent implements SecurityRule {
13
+ readonly id = "AP001";
14
+ readonly name = "Public without explicit intent";
15
+ readonly description = "Endpoint is publicly accessible without explicit @Public or allowAnonymous marker";
16
+ readonly severity = Severity.High;
17
+ evaluate(endpoint: Endpoint): Finding[];
18
+ private getRecommendation;
19
+ }
20
+ //# sourceMappingURL=public-without-explicit-intent.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { createFinding } from '../../core/models/finding.js';
2
+ import { Severity } from '../../core/models/severity.js';
3
+ import { SecurityClassification } from '../../core/models/security-classification.js';
4
+ /**
5
+ * AP001: Public without explicit intent
6
+ *
7
+ * Detects endpoints that are publicly accessible without an explicit
8
+ * @Public decorator or allowAnonymous middleware. This catches endpoints
9
+ * that may be accidentally exposed due to missing authentication.
10
+ */
11
+ export class PublicWithoutExplicitIntent {
12
+ id = 'AP001';
13
+ name = 'Public without explicit intent';
14
+ description = 'Endpoint is publicly accessible without explicit @Public or allowAnonymous marker';
15
+ severity = Severity.High;
16
+ evaluate(endpoint) {
17
+ const findings = [];
18
+ const auth = endpoint.authorization;
19
+ // Only flag if:
20
+ // 1. Endpoint is classified as public
21
+ // 2. No explicit public marker
22
+ // 3. No authentication middleware
23
+ if (auth.classification === SecurityClassification.Public &&
24
+ !auth.isExplicitlyPublic &&
25
+ !auth.isAuthenticated &&
26
+ auth.guardNames.length === 0) {
27
+ findings.push(createFinding({
28
+ ruleId: this.id,
29
+ ruleName: this.name,
30
+ severity: this.severity,
31
+ message: `${endpoint.method} ${endpoint.route} is publicly accessible without explicit intent`,
32
+ endpoint,
33
+ location: endpoint.location,
34
+ recommendation: this.getRecommendation(endpoint),
35
+ }));
36
+ }
37
+ return findings;
38
+ }
39
+ getRecommendation(endpoint) {
40
+ switch (endpoint.type) {
41
+ case 'express':
42
+ return ('Add authentication middleware (e.g., passport.authenticate) or ' +
43
+ 'mark as explicitly public with allowAnonymous middleware if intentional.');
44
+ case 'nestjs':
45
+ return ('Add @UseGuards(AuthGuard) to require authentication, or ' +
46
+ 'add @Public() decorator if public access is intentional.');
47
+ case 'fastify':
48
+ return ('Add preHandler hook for authentication or ' +
49
+ 'mark as explicitly public if intentional.');
50
+ case 'koa':
51
+ return ('Add authentication middleware or ' +
52
+ 'mark as explicitly public if intentional.');
53
+ default:
54
+ return 'Add authentication or mark as explicitly public if intentional.';
55
+ }
56
+ }
57
+ }
58
+ //# sourceMappingURL=public-without-explicit-intent.js.map
@@ -0,0 +1,11 @@
1
+ export * from './rule-interface.js';
2
+ export * from './rule-engine.js';
3
+ export * from './exposure/public-without-explicit-intent.js';
4
+ export * from './exposure/allow-anonymous-on-write.js';
5
+ export * from './consistency/controller-action-conflict.js';
6
+ export * from './consistency/missing-auth-on-writes.js';
7
+ export * from './privilege/excessive-role-access.js';
8
+ export * from './privilege/weak-role-naming.js';
9
+ export * from './surface/sensitive-route-keywords.js';
10
+ export * from './surface/unprotected-endpoint.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,11 @@
1
+ export * from './rule-interface.js';
2
+ export * from './rule-engine.js';
3
+ export * from './exposure/public-without-explicit-intent.js';
4
+ export * from './exposure/allow-anonymous-on-write.js';
5
+ export * from './consistency/controller-action-conflict.js';
6
+ export * from './consistency/missing-auth-on-writes.js';
7
+ export * from './privilege/excessive-role-access.js';
8
+ export * from './privilege/weak-role-naming.js';
9
+ export * from './surface/sensitive-route-keywords.js';
10
+ export * from './surface/unprotected-endpoint.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,20 @@
1
+ import { Endpoint } from '../../core/models/endpoint.js';
2
+ import { Finding } from '../../core/models/finding.js';
3
+ import { Severity } from '../../core/models/severity.js';
4
+ import { SecurityRule } from '../rule-interface.js';
5
+ /**
6
+ * AP005: Excessive role access
7
+ *
8
+ * Detects endpoints that allow too many roles (>3 by default).
9
+ * This may indicate overly permissive access control or a need
10
+ * for refactoring the authorization model.
11
+ */
12
+ export declare class ExcessiveRoleAccess implements SecurityRule {
13
+ readonly id = "AP005";
14
+ readonly name = "Excessive role access";
15
+ readonly description = "Endpoint allows more than 3 roles";
16
+ readonly severity = Severity.Low;
17
+ private readonly maxRoles;
18
+ evaluate(endpoint: Endpoint): Finding[];
19
+ }
20
+ //# sourceMappingURL=excessive-role-access.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { createFinding } from '../../core/models/finding.js';
2
+ import { Severity } from '../../core/models/severity.js';
3
+ /**
4
+ * AP005: Excessive role access
5
+ *
6
+ * Detects endpoints that allow too many roles (>3 by default).
7
+ * This may indicate overly permissive access control or a need
8
+ * for refactoring the authorization model.
9
+ */
10
+ export class ExcessiveRoleAccess {
11
+ id = 'AP005';
12
+ name = 'Excessive role access';
13
+ description = 'Endpoint allows more than 3 roles';
14
+ severity = Severity.Low;
15
+ maxRoles = 3;
16
+ evaluate(endpoint) {
17
+ const findings = [];
18
+ const roles = endpoint.authorization.roles;
19
+ if (roles.length > this.maxRoles) {
20
+ findings.push(createFinding({
21
+ ruleId: this.id,
22
+ ruleName: this.name,
23
+ severity: this.severity,
24
+ message: `${endpoint.method} ${endpoint.route} allows ${roles.length} roles: ${roles.join(', ')}`,
25
+ endpoint,
26
+ location: endpoint.location,
27
+ recommendation: `This endpoint allows ${roles.length} different roles which may indicate ` +
28
+ 'overly permissive access. Consider: (1) Creating a permission-based system ' +
29
+ 'instead of role-based, (2) Creating role hierarchies, or (3) Using policies ' +
30
+ 'to combine related access patterns.',
31
+ }));
32
+ }
33
+ return findings;
34
+ }
35
+ }
36
+ //# sourceMappingURL=excessive-role-access.js.map
@@ -0,0 +1,20 @@
1
+ import { Endpoint } from '../../core/models/endpoint.js';
2
+ import { Finding } from '../../core/models/finding.js';
3
+ import { Severity } from '../../core/models/severity.js';
4
+ import { SecurityRule } from '../rule-interface.js';
5
+ /**
6
+ * AP006: Weak role naming
7
+ *
8
+ * Detects generic or weak role names that don't clearly define
9
+ * the access level or purpose. Examples: 'User', 'Admin', 'Guest'.
10
+ * More specific names like 'billing-admin' or 'content-editor' are preferred.
11
+ */
12
+ export declare class WeakRoleNaming implements SecurityRule {
13
+ readonly id = "AP006";
14
+ readonly name = "Weak role naming";
15
+ readonly description = "Role names are too generic or weak";
16
+ readonly severity = Severity.Low;
17
+ private readonly weakPatterns;
18
+ evaluate(endpoint: Endpoint): Finding[];
19
+ }
20
+ //# sourceMappingURL=weak-role-naming.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { createFinding } from '../../core/models/finding.js';
2
+ import { Severity } from '../../core/models/severity.js';
3
+ /**
4
+ * AP006: Weak role naming
5
+ *
6
+ * Detects generic or weak role names that don't clearly define
7
+ * the access level or purpose. Examples: 'User', 'Admin', 'Guest'.
8
+ * More specific names like 'billing-admin' or 'content-editor' are preferred.
9
+ */
10
+ export class WeakRoleNaming {
11
+ id = 'AP006';
12
+ name = 'Weak role naming';
13
+ description = 'Role names are too generic or weak';
14
+ severity = Severity.Low;
15
+ weakPatterns = [
16
+ /^user$/i,
17
+ /^admin$/i,
18
+ /^guest$/i,
19
+ /^member$/i,
20
+ /^moderator$/i,
21
+ /^manager$/i,
22
+ /^superuser$/i,
23
+ /^root$/i,
24
+ /^default$/i,
25
+ /^basic$/i,
26
+ /^standard$/i,
27
+ /^premium$/i,
28
+ /^vip$/i,
29
+ ];
30
+ evaluate(endpoint) {
31
+ const findings = [];
32
+ const roles = endpoint.authorization.roles;
33
+ const weakRoles = roles.filter((role) => this.weakPatterns.some((pattern) => pattern.test(role)));
34
+ if (weakRoles.length > 0) {
35
+ findings.push(createFinding({
36
+ ruleId: this.id,
37
+ ruleName: this.name,
38
+ severity: this.severity,
39
+ message: `${endpoint.method} ${endpoint.route} uses generic role names: ${weakRoles.join(', ')}`,
40
+ endpoint,
41
+ location: endpoint.location,
42
+ recommendation: 'Consider using more descriptive role names that indicate specific permissions ' +
43
+ `or responsibilities. Instead of "${weakRoles[0]}", consider names like ` +
44
+ '"billing-admin", "content-editor", "inventory-manager", or "read-only-analyst".',
45
+ }));
46
+ }
47
+ return findings;
48
+ }
49
+ }
50
+ //# sourceMappingURL=weak-role-naming.js.map
@@ -0,0 +1,15 @@
1
+ import { Endpoint } from '../core/models/endpoint.js';
2
+ import { Finding } from '../core/models/finding.js';
3
+ import { SecurityRule, RuleConfig } from './rule-interface.js';
4
+ export interface RuleEngineConfig {
5
+ rules?: Record<string, RuleConfig>;
6
+ }
7
+ export declare class RuleEngine {
8
+ private rules;
9
+ constructor(config?: RuleEngineConfig);
10
+ private initializeRules;
11
+ evaluate(endpoints: Endpoint[]): Finding[];
12
+ getRules(): SecurityRule[];
13
+ getRuleById(id: string): SecurityRule | undefined;
14
+ }
15
+ //# sourceMappingURL=rule-engine.d.ts.map
@@ -0,0 +1,52 @@
1
+ // Import all rules
2
+ import { PublicWithoutExplicitIntent } from './exposure/public-without-explicit-intent.js';
3
+ import { AllowAnonymousOnWrite } from './exposure/allow-anonymous-on-write.js';
4
+ import { ControllerActionConflict } from './consistency/controller-action-conflict.js';
5
+ import { MissingAuthOnWrites } from './consistency/missing-auth-on-writes.js';
6
+ import { ExcessiveRoleAccess } from './privilege/excessive-role-access.js';
7
+ import { WeakRoleNaming } from './privilege/weak-role-naming.js';
8
+ import { SensitiveRouteKeywords } from './surface/sensitive-route-keywords.js';
9
+ import { UnprotectedEndpoint } from './surface/unprotected-endpoint.js';
10
+ export class RuleEngine {
11
+ rules = [];
12
+ constructor(config) {
13
+ this.initializeRules(config);
14
+ }
15
+ initializeRules(config) {
16
+ const allRules = [
17
+ new PublicWithoutExplicitIntent(),
18
+ new AllowAnonymousOnWrite(),
19
+ new ControllerActionConflict(),
20
+ new MissingAuthOnWrites(),
21
+ new ExcessiveRoleAccess(),
22
+ new WeakRoleNaming(),
23
+ new SensitiveRouteKeywords(),
24
+ new UnprotectedEndpoint(),
25
+ ];
26
+ // Filter and configure rules based on config
27
+ for (const rule of allRules) {
28
+ const ruleConfig = config?.rules?.[rule.id];
29
+ if (ruleConfig?.enabled === false) {
30
+ continue;
31
+ }
32
+ this.rules.push(rule);
33
+ }
34
+ }
35
+ evaluate(endpoints) {
36
+ const findings = [];
37
+ for (const endpoint of endpoints) {
38
+ for (const rule of this.rules) {
39
+ const ruleFindings = rule.evaluate(endpoint);
40
+ findings.push(...ruleFindings);
41
+ }
42
+ }
43
+ return findings;
44
+ }
45
+ getRules() {
46
+ return [...this.rules];
47
+ }
48
+ getRuleById(id) {
49
+ return this.rules.find((r) => r.id === id);
50
+ }
51
+ }
52
+ //# sourceMappingURL=rule-engine.js.map
@@ -0,0 +1,16 @@
1
+ import { Endpoint } from '../core/models/endpoint.js';
2
+ import { Finding } from '../core/models/finding.js';
3
+ import { Severity } from '../core/models/severity.js';
4
+ export interface SecurityRule {
5
+ readonly id: string;
6
+ readonly name: string;
7
+ readonly description: string;
8
+ readonly severity: Severity;
9
+ evaluate(endpoint: Endpoint): Finding[];
10
+ }
11
+ export interface RuleConfig {
12
+ enabled: boolean;
13
+ severity?: Severity;
14
+ options?: Record<string, unknown>;
15
+ }
16
+ //# sourceMappingURL=rule-interface.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rule-interface.js.map
@@ -0,0 +1,20 @@
1
+ import { Endpoint } from '../../core/models/endpoint.js';
2
+ import { Finding } from '../../core/models/finding.js';
3
+ import { Severity } from '../../core/models/severity.js';
4
+ import { SecurityRule } from '../rule-interface.js';
5
+ /**
6
+ * AP007: Sensitive route keywords
7
+ *
8
+ * Detects publicly accessible routes that contain sensitive keywords
9
+ * like 'admin', 'debug', 'internal', 'export', etc. These routes
10
+ * typically should not be publicly accessible.
11
+ */
12
+ export declare class SensitiveRouteKeywords implements SecurityRule {
13
+ readonly id = "AP007";
14
+ readonly name = "Sensitive route keywords";
15
+ readonly description = "Public route contains sensitive keywords";
16
+ readonly severity = Severity.Medium;
17
+ private readonly sensitiveKeywords;
18
+ evaluate(endpoint: Endpoint): Finding[];
19
+ }
20
+ //# sourceMappingURL=sensitive-route-keywords.d.ts.map