@sun-asterisk/sunlint 1.3.18 → 1.3.20

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 (35) hide show
  1. package/config/rules/enhanced-rules-registry.json +77 -18
  2. package/core/cli-program.js +9 -1
  3. package/core/github-annotate-service.js +986 -0
  4. package/core/output-service.js +294 -6
  5. package/core/summary-report-service.js +30 -30
  6. package/docs/GITHUB_ACTIONS_INTEGRATION.md +421 -0
  7. package/package.json +2 -1
  8. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
  9. package/rules/common/C017_constructor_logic/analyzer.js +137 -503
  10. package/rules/common/C017_constructor_logic/config.json +50 -0
  11. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
  12. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
  13. package/rules/security/S011_secure_guid_generation/README.md +255 -0
  14. package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
  15. package/rules/security/S011_secure_guid_generation/config.json +56 -0
  16. package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
  17. package/rules/security/S028_file_upload_size_limits/README.md +537 -0
  18. package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
  19. package/rules/security/S028_file_upload_size_limits/config.json +186 -0
  20. package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
  21. package/rules/security/S041_session_token_invalidation/README.md +303 -0
  22. package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
  23. package/rules/security/S041_session_token_invalidation/config.json +175 -0
  24. package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
  25. package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
  26. package/rules/security/S044_re_authentication_required/README.md +136 -0
  27. package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
  28. package/rules/security/S044_re_authentication_required/config.json +161 -0
  29. package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
  30. package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
  31. package/rules/security/S045_brute_force_protection/README.md +345 -0
  32. package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
  33. package/rules/security/S045_brute_force_protection/config.json +139 -0
  34. package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
  35. package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
@@ -34,21 +34,59 @@ class OutputService {
34
34
  }
35
35
  }
36
36
 
37
+
37
38
  async outputResults(results, options, metadata = {}) {
38
- // Generate report based on format
39
- const report = this.generateReport(results, metadata, options);
39
+ // Handle GitHub annotation setup
40
+ const githubAnnotateConfig = this._prepareGitHubAnnotation(options);
41
+
42
+ // Generate report based on format (override format to json if github-annotate is enabled)
43
+ const effectiveFormat = githubAnnotateConfig.shouldAnnotate ? 'json' : options.format;
44
+ const report = this.generateReport(results, metadata, { ...options, format: effectiveFormat });
40
45
 
41
46
  // Console output
42
47
  if (!options.quiet) {
43
48
  console.log(report.formatted);
44
49
  }
45
50
 
51
+ // Determine output file (temp or user-specified)
52
+ let outputFile = options.output;
53
+ let shouldCleanupTempFile = false;
54
+
55
+ if (githubAnnotateConfig.shouldAnnotate && !outputFile) {
56
+ // Create temp file for GitHub annotation
57
+ outputFile = githubAnnotateConfig.tempFile;
58
+ shouldCleanupTempFile = true;
59
+ if (options.verbose) {
60
+ console.log(chalk.gray(`ℹ️ Created temporary report file for GitHub annotation: ${outputFile}`));
61
+ }
62
+ }
63
+
46
64
  // File output
47
- if (options.output) {
48
- const outputData = options.format === 'json' ? report.raw : report.formatted;
65
+ if (outputFile) {
66
+ const outputData = effectiveFormat === 'json' ? report.raw : report.formatted;
49
67
  const content = typeof outputData === 'string' ? outputData : JSON.stringify(outputData, null, 2);
50
- fs.writeFileSync(options.output, content);
51
- console.log(chalk.green(`📄 Report saved to: ${options.output}`));
68
+
69
+ try {
70
+ fs.writeFileSync(outputFile, content);
71
+ if (!shouldCleanupTempFile) {
72
+ console.log(chalk.green(`📄 Report saved to: ${outputFile}`));
73
+ }
74
+ } catch (error) {
75
+ console.error(chalk.red(`❌ Failed to write report file: ${error.message}`));
76
+ if (shouldCleanupTempFile) {
77
+ this._cleanupTempFile(outputFile);
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ // GitHub annotation
84
+ if (githubAnnotateConfig.shouldAnnotate) {
85
+ await this._handleGitHubAnnotation(
86
+ githubAnnotateConfig,
87
+ outputFile,
88
+ shouldCleanupTempFile
89
+ );
52
90
  }
53
91
 
54
92
  // Summary report output (new feature for CI/CD)
@@ -474,6 +512,256 @@ class OutputService {
474
512
  };
475
513
  }
476
514
  }
515
+
516
+ /**
517
+ * Prepare GitHub annotation configuration
518
+ * Check environment and prerequisites
519
+ * @param {Object} options - CLI options
520
+ * @returns {Object} Configuration object
521
+ * @private
522
+ */
523
+ _prepareGitHubAnnotation(options) {
524
+ // Check if github-annotate flag is enabled
525
+ if (!options.githubAnnotate) {
526
+ return { shouldAnnotate: false };
527
+ }
528
+
529
+ // Parse mode: true/'all' -> all, 'annotate' -> annotate, 'summary' -> summary
530
+ let mode = 'all'; // default
531
+ if (typeof options.githubAnnotate === 'string') {
532
+ const validModes = ['annotate', 'summary', 'all'];
533
+ if (validModes.includes(options.githubAnnotate.toLowerCase())) {
534
+ mode = options.githubAnnotate.toLowerCase();
535
+ } else {
536
+ console.log(chalk.yellow(`⚠️ Invalid --github-annotate mode: ${options.githubAnnotate}. Using default: all`));
537
+ }
538
+ }
539
+
540
+ // Check if we're in a GitHub Actions environment
541
+ const isGitHubActions = process.env.GITHUB_ACTIONS === 'true';
542
+ if (!isGitHubActions) {
543
+ if (options.verbose) {
544
+ console.log(chalk.yellow('⚠️ --github-annotate only works in GitHub Actions environment'));
545
+ }
546
+ return { shouldAnnotate: false };
547
+ }
548
+
549
+ // Get GitHub environment variables
550
+ const eventName = process.env.GITHUB_EVENT_NAME;
551
+ const repo = process.env.GITHUB_REPOSITORY;
552
+ const githubToken = process.env.GITHUB_TOKEN || options.githubToken;
553
+
554
+ // Check if it's a PR event
555
+ const isPullRequestEvent = eventName === 'pull_request' || eventName === 'pull_request_target';
556
+
557
+ if (!isPullRequestEvent) {
558
+ if (options.verbose) {
559
+ console.log(chalk.yellow(`⚠️ GitHub annotation only works on pull_request events (current: ${eventName})`));
560
+ }
561
+ return { shouldAnnotate: false };
562
+ }
563
+
564
+ // Get PR number from GitHub context
565
+ let prNumber = null;
566
+ try {
567
+ // Try to get PR number from GITHUB_EVENT_PATH
568
+ const eventPath = process.env.GITHUB_EVENT_PATH;
569
+ if (eventPath && fs.existsSync(eventPath)) {
570
+ const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
571
+ prNumber = event.pull_request?.number;
572
+ }
573
+ } catch (error) {
574
+ if (options.verbose) {
575
+ console.log(chalk.yellow(`⚠️ Failed to read GitHub event data: ${error.message}`));
576
+ }
577
+ }
578
+
579
+ // Fallback: try from environment variable
580
+ if (!prNumber && process.env.GITHUB_PR_NUMBER) {
581
+ prNumber = parseInt(process.env.GITHUB_PR_NUMBER, 10);
582
+ }
583
+
584
+ // Validate required data
585
+ if (!repo) {
586
+ console.log(chalk.yellow('⚠️ Missing GITHUB_REPOSITORY environment variable'));
587
+ return { shouldAnnotate: false };
588
+ }
589
+
590
+ if (!prNumber) {
591
+ console.log(chalk.yellow('⚠️ Could not determine PR number from GitHub context'));
592
+ return { shouldAnnotate: false };
593
+ }
594
+
595
+ if (!githubToken) {
596
+ console.log(chalk.yellow('⚠️ Missing GITHUB_TOKEN for authentication'));
597
+ return { shouldAnnotate: false };
598
+ }
599
+
600
+ // Generate temp file path if needed
601
+ const tempFile = path.join(
602
+ process.env.RUNNER_TEMP || '/tmp',
603
+ `sunlint-report-${Date.now()}-${Math.random().toString(36).substring(2, 11)}.json`
604
+ );
605
+
606
+ return {
607
+ shouldAnnotate: true,
608
+ mode,
609
+ repo,
610
+ prNumber,
611
+ githubToken,
612
+ tempFile,
613
+ eventName
614
+ };
615
+ }
616
+
617
+ /**
618
+ * Handle GitHub annotation process
619
+ * @param {Object} config - GitHub annotation configuration
620
+ * @param {string} outputFile - Path to report file
621
+ * @param {boolean} shouldCleanup - Whether to cleanup temp file
622
+ * @private
623
+ */
624
+ async _handleGitHubAnnotation(config, outputFile, shouldCleanup) {
625
+ const mode = config.mode || 'all';
626
+ const results = {};
627
+
628
+ try {
629
+ console.log(chalk.blue(`🔄 GitHub PR annotation mode: ${mode}`));
630
+
631
+ if (!config.repo || !config.prNumber || !config.githubToken) {
632
+ throw new Error('Missing required GitHub configuration');
633
+ }
634
+
635
+ if (!outputFile || !fs.existsSync(outputFile)) {
636
+ throw new Error(`Report file not found: ${outputFile}`);
637
+ }
638
+
639
+ // Import services
640
+ const { annotate, postSummaryComment } = require('./github-annotate-service');
641
+
642
+ // Execute based on mode
643
+ const shouldAnnotate = mode === 'annotate' || mode === 'all';
644
+ const shouldSummary = mode === 'summary' || mode === 'all';
645
+
646
+ // 1. Inline comments (annotate mode)
647
+ if (shouldAnnotate) {
648
+ try {
649
+ console.log(chalk.blue('📝 Creating inline comments...'));
650
+ const annotateResult = await annotate({
651
+ jsonFile: outputFile,
652
+ githubToken: config.githubToken,
653
+ repo: config.repo,
654
+ prNumber: config.prNumber,
655
+ skipDuplicates: true
656
+ });
657
+
658
+ results.annotate = annotateResult;
659
+
660
+ if (annotateResult.success) {
661
+ console.log(chalk.green(`✅ Inline comments: ${annotateResult.stats.commentsCreated} created`));
662
+ if (annotateResult.stats.duplicatesSkipped > 0) {
663
+ console.log(chalk.gray(` • Duplicates skipped: ${annotateResult.stats.duplicatesSkipped}`));
664
+ }
665
+ }
666
+ } catch (error) {
667
+ console.log(chalk.red(`❌ Failed to create inline comments: ${error.message}`));
668
+ results.annotate = { success: false, error: error.message };
669
+
670
+ // Don't throw if we still need to create summary
671
+ if (!shouldSummary) {
672
+ throw error;
673
+ }
674
+ }
675
+ }
676
+
677
+ // 2. Summary comment (summary mode)
678
+ if (shouldSummary) {
679
+ try {
680
+ console.log(chalk.blue('💬 Creating summary comment...'));
681
+ const summaryResult = await postSummaryComment({
682
+ jsonFile: outputFile,
683
+ githubToken: config.githubToken,
684
+ repo: config.repo,
685
+ prNumber: config.prNumber
686
+ });
687
+
688
+ results.summary = summaryResult;
689
+
690
+ if (summaryResult.success) {
691
+ console.log(chalk.green(`✅ Summary comment: ${summaryResult.action}`));
692
+ if (summaryResult.stats) {
693
+ console.log(chalk.gray(` • Total violations: ${summaryResult.stats.totalViolations}`));
694
+ console.log(chalk.gray(` • Errors: ${summaryResult.stats.errorCount}, Warnings: ${summaryResult.stats.warningCount}`));
695
+ }
696
+ }
697
+ } catch (error) {
698
+ console.log(chalk.red(`❌ Failed to create summary comment: ${error.message}`));
699
+ results.summary = { success: false, error: error.message };
700
+
701
+ // Throw if both failed or if this is the only mode
702
+ if (!results.annotate || !results.annotate.success) {
703
+ throw error;
704
+ }
705
+ }
706
+ }
707
+
708
+ // Final summary
709
+ const successCount = [results.annotate?.success, results.summary?.success].filter(Boolean).length;
710
+ const totalCount = [shouldAnnotate, shouldSummary].filter(Boolean).length;
711
+
712
+ if (successCount === totalCount) {
713
+ console.log(chalk.green(`\n✅ Successfully annotated PR #${config.prNumber} (${successCount}/${totalCount} tasks completed)`));
714
+ } else if (successCount > 0) {
715
+ console.log(chalk.yellow(`\n⚠️ Partially completed (${successCount}/${totalCount} tasks successful)`));
716
+ } else {
717
+ console.log(chalk.red(`\n❌ Annotation failed`));
718
+ }
719
+
720
+ } catch (error) {
721
+ console.log(chalk.red(`\n❌ Failed to annotate GitHub PR: ${error.message}`));
722
+
723
+ // Log detailed error in verbose mode
724
+ if (process.env.DEBUG === 'true' && error.stack) {
725
+ console.error(chalk.gray('Error stack:'), error.stack);
726
+ }
727
+
728
+ // Show suggestions based on error type
729
+ if (error.name === 'ValidationError') {
730
+ console.log(chalk.yellow('💡 Hint: Check your GitHub environment variables'));
731
+ } else if (error.name === 'GitHubAPIError') {
732
+ console.log(chalk.yellow('💡 Hint: Check GitHub token permissions (needs pull-requests:write)'));
733
+ }
734
+
735
+ } finally {
736
+ // Cleanup temp file if needed
737
+ if (shouldCleanup) {
738
+ this._cleanupTempFile(outputFile);
739
+ }
740
+ }
741
+
742
+ return results;
743
+ }
744
+
745
+ /**
746
+ * Cleanup temporary file
747
+ * @param {string} filePath - Path to temp file
748
+ * @private
749
+ */
750
+ _cleanupTempFile(filePath) {
751
+ try {
752
+ if (filePath && fs.existsSync(filePath)) {
753
+ fs.unlinkSync(filePath);
754
+ if (process.env.DEBUG === 'true') {
755
+ console.log(chalk.gray(`🗑️ Cleaned up temp file: ${filePath}`));
756
+ }
757
+ }
758
+ } catch (error) {
759
+ // Non-critical error, just log in debug mode
760
+ if (process.env.DEBUG === 'true') {
761
+ console.warn(chalk.yellow(`⚠️ Failed to cleanup temp file: ${error.message}`));
762
+ }
763
+ }
764
+ }
477
765
  }
478
766
 
479
767
  module.exports = OutputService;
@@ -13,7 +13,7 @@ class SummaryReportService {
13
13
  // Load version from package.json
14
14
  this.version = this._loadVersion();
15
15
  }
16
-
16
+
17
17
  /**
18
18
  * Load version from package.json
19
19
  * @returns {string} Package version
@@ -23,9 +23,9 @@ class SummaryReportService {
23
23
  try {
24
24
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
25
25
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
26
- return packageJson.version || '1.3.18';
26
+ return packageJson.version || '1.3.19';
27
27
  } catch (error) {
28
- return '1.3.18'; // Fallback version
28
+ return '1.3.19'; // Fallback version
29
29
  }
30
30
  }
31
31
 
@@ -154,44 +154,44 @@ class SummaryReportService {
154
154
  const gitInfo = this.getGitInfo(options.cwd);
155
155
 
156
156
  // Override with environment variables if available (from CI/CD)
157
- const repository_url = process.env.GITHUB_REPOSITORY
157
+ const repository_url = process.env.GITHUB_REPOSITORY
158
158
  ? `https://github.com/${process.env.GITHUB_REPOSITORY}`
159
159
  : gitInfo.repository_url;
160
-
161
- const repository_name = process.env.GITHUB_REPOSITORY
160
+
161
+ const repository_name = process.env.GITHUB_REPOSITORY
162
162
  ? process.env.GITHUB_REPOSITORY.split('/')[1]
163
163
  : (gitInfo.repository_name || null);
164
-
164
+
165
165
  const branch = process.env.GITHUB_REF_NAME || gitInfo.branch;
166
166
  const commit_hash = process.env.GITHUB_SHA || gitInfo.commit_hash;
167
-
167
+
168
168
  // Get commit details from GitHub context or git
169
- const commit_message = process.env.GITHUB_EVENT_HEAD_COMMIT_MESSAGE
170
- || (process.env.GITHUB_EVENT_PATH
169
+ const commit_message = process.env.GITHUB_EVENT_HEAD_COMMIT_MESSAGE
170
+ || (process.env.GITHUB_EVENT_PATH
171
171
  ? this._getGitHubEventData('head_commit.message')
172
172
  : null)
173
173
  || gitInfo.commit_message;
174
-
174
+
175
175
  const author_email = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_EMAIL
176
- || (process.env.GITHUB_EVENT_PATH
176
+ || (process.env.GITHUB_EVENT_PATH
177
177
  ? this._getGitHubEventData('head_commit.author.email')
178
178
  : null)
179
179
  || gitInfo.author_email;
180
-
180
+
181
181
  const author_name = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_NAME
182
- || (process.env.GITHUB_EVENT_PATH
182
+ || (process.env.GITHUB_EVENT_PATH
183
183
  ? this._getGitHubEventData('head_commit.author.name')
184
184
  : null)
185
185
  || gitInfo.author_name;
186
-
186
+
187
187
  // Get PR number from GitHub event or git
188
188
  let pr_number = null;
189
189
  if (process.env.GITHUB_EVENT_PATH) {
190
- pr_number = this._getGitHubEventData('pull_request.number')
190
+ pr_number = this._getGitHubEventData('pull_request.number')
191
191
  || this._getGitHubEventData('number');
192
192
  }
193
193
  pr_number = pr_number || gitInfo.pr_number;
194
-
194
+
195
195
  // Get project path (for mono-repo support)
196
196
  const project_path = gitInfo.project_path;
197
197
 
@@ -203,20 +203,20 @@ class SummaryReportService {
203
203
  if (!violation || typeof violation !== 'object') {
204
204
  return; // Skip non-objects
205
205
  }
206
-
206
+
207
207
  // Skip objects that look like metadata/config (have nested objects like semanticEngine, project, etc.)
208
208
  if (violation.semanticEngine || violation.project || violation.options) {
209
209
  return; // Skip config objects
210
210
  }
211
-
211
+
212
212
  // Get ruleId from various possible fields
213
213
  const ruleId = violation.ruleId || violation.rule || 'unknown';
214
-
214
+
215
215
  // Ensure ruleId is a string (not an object)
216
216
  if (typeof ruleId !== 'string') {
217
217
  return; // Skip invalid ruleId
218
218
  }
219
-
219
+
220
220
  if (!violationsByRule[ruleId]) {
221
221
  violationsByRule[ruleId] = {
222
222
  rule_code: ruleId,
@@ -253,7 +253,7 @@ class SummaryReportService {
253
253
  sunlint_version: options.version || this.version,
254
254
  analysis_duration_ms: options.duration || 0,
255
255
  violations: violationsSummary,
256
-
256
+
257
257
  // Additional metadata for backwards compatibility
258
258
  metadata: {
259
259
  generated_at: new Date().toISOString(),
@@ -283,11 +283,11 @@ class SummaryReportService {
283
283
  if (!eventPath || !fs.existsSync(eventPath)) {
284
284
  return null;
285
285
  }
286
-
286
+
287
287
  const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
288
288
  const keys = path.split('.');
289
289
  let value = eventData;
290
-
290
+
291
291
  for (const key of keys) {
292
292
  if (value && typeof value === 'object' && key in value) {
293
293
  value = value[key];
@@ -295,7 +295,7 @@ class SummaryReportService {
295
295
  return null;
296
296
  }
297
297
  }
298
-
298
+
299
299
  return value;
300
300
  } catch (error) {
301
301
  return null;
@@ -311,7 +311,7 @@ class SummaryReportService {
311
311
  */
312
312
  saveSummaryReport(violations, scoringSummary, outputPath, options = {}) {
313
313
  const summaryReport = this.generateSummaryReport(violations, scoringSummary, options);
314
-
314
+
315
315
  // Ensure directory exists
316
316
  const dir = path.dirname(outputPath);
317
317
  if (!fs.existsSync(dir)) {
@@ -320,7 +320,7 @@ class SummaryReportService {
320
320
 
321
321
  // Write to file with pretty formatting
322
322
  fs.writeFileSync(outputPath, JSON.stringify(summaryReport, null, 2), 'utf8');
323
-
323
+
324
324
  return summaryReport;
325
325
  }
326
326
 
@@ -340,7 +340,7 @@ class SummaryReportService {
340
340
  const warningCount = summaryReport.warning_count || 0;
341
341
  const violationsByRule = summaryReport.violations || [];
342
342
  const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0;
343
-
343
+
344
344
  let output = '\n📊 Quality Summary Report\n';
345
345
  output += '━'.repeat(50) + '\n';
346
346
  output += `📈 Quality Score: ${score} (Grade: ${grade})\n`;
@@ -348,14 +348,14 @@ class SummaryReportService {
348
348
  output += `📏 Lines of Code: ${linesOfCode.toLocaleString()}\n`;
349
349
  output += `⚠️ Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`;
350
350
  output += `📊 Violations per KLOC: ${violationsPerKLOC}\n`;
351
-
351
+
352
352
  if (violationsByRule.length > 0) {
353
353
  output += '\n🔍 Top Violations by Rule:\n';
354
354
  violationsByRule.slice(0, 10).forEach((item, index) => {
355
355
  output += ` ${index + 1}. ${item.rule_code}: ${item.count} violations (${item.severity})\n`;
356
356
  });
357
357
  }
358
-
358
+
359
359
  return output;
360
360
  }
361
361
  }