x-fidelity 3.11.0 → 3.12.0

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/.xfi-config.json CHANGED
@@ -51,10 +51,6 @@
51
51
  },
52
52
  "codeOwners": true,
53
53
  "notifyOnSuccess": false,
54
- "notifyOnFailure": true,
55
- "customTemplates": {
56
- "success": "# Success! 🎉\n\nYour codebase passed all X-Fidelity checks.\n\n- Archetype: ${archetype}\n- Files analyzed: ${fileCount}\n- Execution time: ${executionTime} seconds\n\nGreat job keeping the code clean!",
57
- "failure": "# Issues Detected ⚠️\n\nX-Fidelity found issues in your codebase:\n\n- Archetype: ${archetype}\n- Files analyzed: ${fileCount}\n- Total issues: ${totalIssues}\n - Warnings: ${warningCount}\n - Errors: ${errorCount}\n - Fatalities: ${fatalityCount}\n\n## Affected Files\n${affectedFiles}\n\nPlease address these issues as soon as possible."
58
- }
54
+ "notifyOnFailure": true
59
55
  }
60
56
  }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # [3.12.0](https://github.com/zotoio/x-fidelity/compare/v3.11.0...v3.12.0) (2025-03-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add HTML email support with plain text fallback ([ec0f252](https://github.com/zotoio/x-fidelity/commit/ec0f2529d4f05ca570cd4b11db07ede2d4d21113))
7
+ * correct string interpolation syntax in notification template ([092b64d](https://github.com/zotoio/x-fidelity/commit/092b64d3f614d505c7a792bd7a559e10aed3192c))
8
+ * correct template literal syntax in notification manager ([f5720c2](https://github.com/zotoio/x-fidelity/commit/f5720c230051d8e38bbc95024a02d7298d2299f5))
9
+ * escape nested template literals in notification template ([4feb497](https://github.com/zotoio/x-fidelity/commit/4feb497255615b93655ba7bba8729e55b56b4016))
10
+ * **notify:** email formatting fixes ([06e80a8](https://github.com/zotoio/x-fidelity/commit/06e80a81bab30c1ab03d202d0649306fe331debd))
11
+ * remove duplicate fileIssues declaration in notification manager ([d5678a1](https://github.com/zotoio/x-fidelity/commit/d5678a1620fae5b78ee475b95ee2248051d17c8f))
12
+ * remove local directory paths from notification file links ([d55da96](https://github.com/zotoio/x-fidelity/commit/d55da9637477b0b14acb45d99a8a1a4b9e661c30))
13
+ * remove trailing comma in .xfi-config.json ([41c7368](https://github.com/zotoio/x-fidelity/commit/41c7368a7e7ba8b989f3d8bf2fc0ce9e3e4d1183))
14
+ * replace nested template literals with string concatenation ([89712cf](https://github.com/zotoio/x-fidelity/commit/89712cf77e231863e5bcb6c214fedf5b9f0d1f3f))
15
+ * resolve TypeScript errors with yaml imports and variable declarations ([af2ea8e](https://github.com/zotoio/x-fidelity/commit/af2ea8ee80d48b9dddb1f586d61285518aee0b1c))
16
+ * use repoPath from results metadata instead of undefined variable ([36b42fa](https://github.com/zotoio/x-fidelity/commit/36b42fae14895ba147955806efcf3cc94ec1d306))
17
+
18
+
19
+ ### Features
20
+
21
+ * add failed rule names to notification template for each file ([d09d019](https://github.com/zotoio/x-fidelity/commit/d09d019bc1f9eff2f5c375185aec7f8f54fb7f96))
22
+ * add severity levels to failed rules in notification output ([a90ffe7](https://github.com/zotoio/x-fidelity/commit/a90ffe724c440f60f1f1c59be0ec3074316c28c1))
23
+ * add YAML results attachment to email notifications ([e2671b4](https://github.com/zotoio/x-fidelity/commit/e2671b46081168b1ac911f595a05f3301f8df780))
24
+ * enhance notification formatting with HTML and color-coded severity ([f034a85](https://github.com/zotoio/x-fidelity/commit/f034a858c0da629d7fc134818babcbf28fa7b03e))
25
+ * enhance notification templates with clickable links and emojis ([3afbd5d](https://github.com/zotoio/x-fidelity/commit/3afbd5db68cfbb604407e791f58c184961e7dc78))
26
+ * support GitHub Enterprise links in notifications ([092867f](https://github.com/zotoio/x-fidelity/commit/092867fd3948e1bfcc10e8ecfc312686f37c9afa))
27
+ * use git commands to extract GitHub repo details ([8f35471](https://github.com/zotoio/x-fidelity/commit/8f3547145a612444b9320c0b5d1b75529edfaa4a))
28
+
1
29
  # [3.11.0](https://github.com/zotoio/x-fidelity/compare/v3.10.0...v3.11.0) (2025-03-09)
2
30
 
3
31
 
@@ -12,10 +12,10 @@
12
12
  "fact": "globalFileAnalysis",
13
13
  "params": {
14
14
  "newPatterns": [
15
- "logger\\.info\\("
15
+ "const plugin: XFiPlugin = {"
16
16
  ],
17
17
  "legacyPatterns": [
18
- "logger\\.debug\\("
18
+ "import {.*FactDefn.*} from '\\.\\./types/typeDefs';"
19
19
  ],
20
20
  "fileFilter": ".*\\.(ts|js)$",
21
21
  "resultFact": "sdkUsageAnalysis"
@@ -11,7 +11,11 @@
11
11
  {
12
12
  "fact": "repoFileAnalysis",
13
13
  "params": {
14
- "checkPattern": ["oracle", "mysql", "mssql", "postgres", "sqlite", "mongodb", "cassandra", "redis", "rethinkdb", "neo4j", "couchdb"],
14
+ "checkPattern": [
15
+ "[\\s\\'\\\"\\.](oracle)[\\s\\'\\\"\\.]",
16
+ "[\\s\\'\\\"\\.](postgres)[\\s\\'\\\"\\.]",
17
+ "[\\s\\'\\\"\\.](mongodb)[\\s\\'\\\"\\.]"
18
+ ],
15
19
  "resultFact": "fileResultsDB"
16
20
  },
17
21
  "operator": "fileContains",
@@ -23,11 +23,9 @@
23
23
  "checkPattern": [
24
24
  "(api[_-]?key|auth[_-]?token|access[_-]?token|secret[_-]?key)",
25
25
  "(aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key)",
26
- "(password|passphrase)",
27
26
  "(private[_-]?key|ssh[_-]?key)",
28
27
  "(oauth[_-]?token|jwt[_-]?token)",
29
- "db[_-]?password",
30
- "(declare)"
28
+ "db[_-]?password"
31
29
  ],
32
30
  "resultFact": "fileResults"
33
31
  },
package/dist/index.js CHANGED
@@ -123,14 +123,11 @@ function main() {
123
123
  // Send notifications if enabled
124
124
  if (notificationConfig.enabled) {
125
125
  logger_1.logger.debug('Notifications are enabled, preparing to send report');
126
- // Get list of affected files (those with issues)
127
- const affectedFiles = getAffectedFiles(resultMetadata);
128
126
  logger_1.logger.debug({
129
- affectedFilesCount: affectedFiles.length,
130
- hasRepoConfig: !!resultMetadata.XFI_RESULT.repoXFIConfig
127
+ affectedFilesCount: resultMetadata.XFI_RESULT.issueDetails.length
131
128
  }, 'Preparing notification data');
132
129
  // Pass the repo config to the notification manager
133
- yield notificationManager.sendReport(resultMetadata, affectedFiles, resultMetadata.XFI_RESULT.repoXFIConfig);
130
+ yield notificationManager.sendReport(resultMetadata);
134
131
  }
135
132
  // if results are found, there were issues found in the codebase
136
133
  if (resultMetadata.XFI_RESULT.totalIssues > 0) {
@@ -171,18 +168,6 @@ function main() {
171
168
  }
172
169
  });
173
170
  }
174
- // Helper function to extract affected files from results
175
- function getAffectedFiles(resultMetadata) {
176
- const affectedFiles = [];
177
- if (resultMetadata.XFI_RESULT.issueDetails) {
178
- for (const issue of resultMetadata.XFI_RESULT.issueDetails) {
179
- if (issue.filePath && !affectedFiles.includes(issue.filePath)) {
180
- affectedFiles.push(issue.filePath);
181
- }
182
- }
183
- }
184
- return affectedFiles;
185
- }
186
171
  var configManager_1 = require("./core/configManager");
187
172
  Object.defineProperty(exports, "repoDir", { enumerable: true, get: function () { return configManager_1.repoDir; } });
188
173
  __exportStar(require("./utils/logger"), exports);
@@ -56,6 +56,7 @@ jest.mock('./utils/logger', () => ({
56
56
  warn: jest.fn(),
57
57
  error: jest.fn(),
58
58
  debug: jest.fn(),
59
+ trace: jest.fn()
59
60
  },
60
61
  setLogPrefix: jest.fn(),
61
62
  setLogLevel: jest.fn(),
@@ -1,4 +1,4 @@
1
- import { ResultMetadata, RepoXFIConfig } from '../types/typeDefs';
1
+ import { ResultMetadata } from '../types/typeDefs';
2
2
  import { NotificationProvider, NotificationConfig } from '../types/notificationTypes';
3
3
  export declare class NotificationManager {
4
4
  private static instance;
@@ -8,12 +8,12 @@ export declare class NotificationManager {
8
8
  private constructor();
9
9
  static getInstance(config: NotificationConfig): NotificationManager;
10
10
  registerProvider(provider: NotificationProvider): void;
11
- sendReport(results: ResultMetadata, affectedFiles: string[], repoXFIConfig?: RepoXFIConfig): Promise<void>;
11
+ sendReport(results: ResultMetadata): Promise<void>;
12
12
  private mergeNotificationConfig;
13
13
  private getRecipients;
14
14
  private loadCodeOwners;
15
15
  private getCodeOwnersForFiles;
16
16
  private matchesGlob;
17
- private applyTemplate;
17
+ private getAffectedFiles;
18
18
  private generateReportContent;
19
19
  }
@@ -14,7 +14,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.NotificationManager = void 0;
16
16
  const logger_1 = require("../utils/logger");
17
+ const child_process_1 = require("child_process");
17
18
  const fs_1 = __importDefault(require("fs"));
19
+ const yaml_1 = require("yaml");
18
20
  class NotificationManager {
19
21
  constructor(config) {
20
22
  this.providers = new Map();
@@ -36,17 +38,16 @@ class NotificationManager {
36
38
  this.providers.set(provider.name, provider);
37
39
  logger_1.logger.info(`Registered notification provider: ${provider.name}`);
38
40
  }
39
- sendReport(results, affectedFiles, repoXFIConfig) {
41
+ sendReport(results) {
40
42
  return __awaiter(this, void 0, void 0, function* () {
41
- var _a, _b;
42
43
  if (!this.config.enabled) {
43
44
  logger_1.logger.debug('Notifications are disabled');
44
45
  return;
45
46
  }
46
47
  // Merge global config with repo-specific config
47
- const notifyConfig = this.mergeNotificationConfig(repoXFIConfig);
48
+ const notifyConfig = this.mergeNotificationConfig(results.XFI_RESULT.repoXFIConfig);
48
49
  // Determine if we should send notification based on results
49
- const hasIssues = results.XFI_RESULT.totalIssues > 0;
50
+ const hasIssues = results.XFI_RESULT.issueDetails.length > 0;
50
51
  if (hasIssues && !notifyConfig.notifyOnFailure) {
51
52
  logger_1.logger.debug('Skipping notification for failure as notifyOnFailure is disabled');
52
53
  return;
@@ -56,7 +57,7 @@ class NotificationManager {
56
57
  return;
57
58
  }
58
59
  // Get recipients from multiple sources
59
- const recipients = this.getRecipients(affectedFiles, repoXFIConfig, notifyConfig);
60
+ const recipients = this.getRecipients(this.getAffectedFiles(results), results.XFI_RESULT.repoXFIConfig, notifyConfig);
60
61
  if (Object.values(recipients).every(list => list.length === 0)) {
61
62
  logger_1.logger.warn('No recipients found for notification');
62
63
  return;
@@ -65,12 +66,7 @@ class NotificationManager {
65
66
  const subject = hasIssues
66
67
  ? `[X-Fidelity] Issues found in your codebase (${results.XFI_RESULT.totalIssues})`
67
68
  : '[X-Fidelity] Your codebase passed all checks';
68
- // Use custom template if available
69
- const templateKey = hasIssues ? 'failure' : 'success';
70
- const customTemplate = (_b = (_a = repoXFIConfig === null || repoXFIConfig === void 0 ? void 0 : repoXFIConfig.notifications) === null || _a === void 0 ? void 0 : _a.customTemplates) === null || _b === void 0 ? void 0 : _b[templateKey];
71
- const content = customTemplate
72
- ? this.applyTemplate(customTemplate, results, affectedFiles)
73
- : this.generateReportContent(results, affectedFiles);
69
+ const content = this.generateReportContent(results);
74
70
  // Send through all configured providers
75
71
  for (const providerName of this.config.providers) {
76
72
  const provider = this.providers.get(providerName);
@@ -208,42 +204,141 @@ class NotificationManager {
208
204
  }
209
205
  return filePath === pattern;
210
206
  }
211
- applyTemplate(template, results, affectedFiles) {
212
- // Replace template variables with actual values
213
- return template
214
- .replace(/\${archetype}/g, results.XFI_RESULT.archetype)
215
- .replace(/\${fileCount}/g, String(results.XFI_RESULT.fileCount))
216
- .replace(/\${totalIssues}/g, String(results.XFI_RESULT.totalIssues))
217
- .replace(/\${warningCount}/g, String(results.XFI_RESULT.warningCount))
218
- .replace(/\${errorCount}/g, String(results.XFI_RESULT.errorCount))
219
- .replace(/\${fatalityCount}/g, String(results.XFI_RESULT.fatalityCount))
220
- .replace(/\${exemptCount}/g, String(results.XFI_RESULT.exemptCount))
221
- .replace(/\${affectedFiles}/g, affectedFiles.map(file => `- ${file}`).join('\n'))
222
- .replace(/\${date}/g, new Date().toISOString())
223
- .replace(/\${executionTime}/g, String(results.XFI_RESULT.durationSeconds));
207
+ // Helper function to extract affected files from results
208
+ getAffectedFiles(results) {
209
+ const affectedFiles = [];
210
+ if (results.XFI_RESULT.issueDetails) {
211
+ for (const issue of results.XFI_RESULT.issueDetails) {
212
+ if (issue.filePath && !affectedFiles.includes(issue.filePath)) {
213
+ affectedFiles.push(issue.filePath);
214
+ }
215
+ }
216
+ }
217
+ return affectedFiles;
224
218
  }
225
- generateReportContent(results, affectedFiles) {
226
- // Basic template - can be enhanced or made configurable
227
- return `
228
- # X-Fidelity Analysis Report
219
+ generateReportContent(results) {
220
+ var _a, _b;
221
+ // Get GitHub details from git commands with fallbacks to env vars
222
+ let githubServer = '';
223
+ let githubRepo = '';
224
+ let githubBranch = '';
225
+ try {
226
+ // Get remote URL and extract server/repo
227
+ const remoteUrl = (_a = (0, child_process_1.execSync)('git config --get remote.origin.url', { encoding: 'utf8', cwd: results.XFI_RESULT.repoPath })) === null || _a === void 0 ? void 0 : _a.toString().trim();
228
+ if (remoteUrl) {
229
+ const match = remoteUrl.match(/^(?:https?:\/\/|git@)([^/:]+)[/:]([^/]+\/[^/.]+)(?:\.git)?$/);
230
+ if (match) {
231
+ githubServer = process.env.GITHUB_SERVER_URL || `https://${match[1]}`;
232
+ githubRepo = process.env.GITHUB_REPOSITORY || match[2];
233
+ }
234
+ }
235
+ // Get current branch
236
+ githubBranch = (_b = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', cwd: results.XFI_RESULT.repoPath })) === null || _b === void 0 ? void 0 : _b.toString().trim();
237
+ }
238
+ catch (error) {
239
+ logger_1.logger.warn('Failed to get git details, falling back to defaults', { error });
240
+ githubServer = process.env.GITHUB_SERVER_URL || 'https://github.com';
241
+ githubRepo = process.env.GITHUB_REPOSITORY || results.XFI_RESULT.repoUrl
242
+ .replace(/\.git$/, '')
243
+ .replace(/^git@([^:]+):/, 'https://$1/')
244
+ .replace(/^https?:\/\/[^\/]+\//, '');
245
+ githubBranch = process.env.GITHUB_REF_NAME || 'main';
246
+ }
247
+ const githubUrl = `${githubServer}/${githubRepo}/blob/${githubBranch}`;
248
+ let fileDetails = '';
249
+ if (results.XFI_RESULT.issueDetails.length > 0) {
250
+ try {
251
+ // Failure template
252
+ fileDetails = results.XFI_RESULT.issueDetails.map(issue => {
253
+ logger_1.logger.debug(issue, `generating report content for issue: ${issue.filePath}`);
254
+ // Remove local path prefix from file paths
255
+ const relativePath = issue.filePath.replace(results.XFI_RESULT.repoPath + '/', '');
256
+ const fileIssues = issue.errors;
257
+ // Start with file name as bold list item with link
258
+ let output = `<li><strong><a href="${githubUrl}/${relativePath}">${relativePath}</a></strong><ul>`;
259
+ // Add rule failures with severity-based colors and line links
260
+ const ruleDetails = fileIssues === null || fileIssues === void 0 ? void 0 : fileIssues.map(error => {
261
+ var _a, _b;
262
+ const ruleName = error.ruleFailure;
263
+ const level = error.level;
264
+ const resultDetails = error === null || error === void 0 ? void 0 : error.details;
265
+ const message = resultDetails === null || resultDetails === void 0 ? void 0 : resultDetails.message;
266
+ let output = `<li>
267
+ <span>${ruleName}: ${level}</span><br>
268
+ <span>${message}</span><ul>
269
+ `;
270
+ if (!Array.isArray((resultDetails === null || resultDetails === void 0 ? void 0 : resultDetails.details) || !((_a = resultDetails === null || resultDetails === void 0 ? void 0 : resultDetails.details) === null || _a === void 0 ? void 0 : _a.details[0].lineNumber))) {
271
+ return '</li>' + output + '<pre>' + (0, yaml_1.stringify)(resultDetails === null || resultDetails === void 0 ? void 0 : resultDetails.details) + '</pre></li></ul></li>';
272
+ }
273
+ const errorDetails = (_b = resultDetails === null || resultDetails === void 0 ? void 0 : resultDetails.details) === null || _b === void 0 ? void 0 : _b.map((errorInstance) => {
274
+ const lineNumber = errorInstance === null || errorInstance === void 0 ? void 0 : errorInstance.lineNumber;
275
+ const lineLink = lineNumber ? `${githubUrl}/${relativePath}#L${lineNumber}` : `${githubUrl}/${relativePath}`;
276
+ const color = level === 'fatality' ? '#f20707' :
277
+ level === 'error' ? '#53046c' :
278
+ level === 'warning' ? '#944801' : '#1a1818';
279
+ return `<li><span style="color: ${color};font-weight:bold">${ruleName}</span>${lineNumber ? ` - <a href="${lineLink}">Line ${lineNumber}</a>` : ''}</li>`;
280
+ }).join('');
281
+ return output + errorDetails + '</ul></li>';
282
+ }).join('');
283
+ logger_1.logger.debug(`fileDetails: ${output + ruleDetails}</ul></li>`);
284
+ return output + ruleDetails + '</ul></li>';
285
+ }).join('');
286
+ }
287
+ catch (error) {
288
+ logger_1.logger.error(error, 'Failed to generate report content');
289
+ logger_1.logger.error(JSON.stringify(error));
290
+ logger_1.logger.error(error.message);
291
+ logger_1.logger.error(error.stack);
292
+ return '';
293
+ }
294
+ // Generate YAML attachment
295
+ const yamlAttachment = (0, yaml_1.stringify)(results.XFI_RESULT);
296
+ const yamlSection = `
297
+ <h2>📎 Full Results (YAML)</h2>
298
+ <pre style="background-color: #d1e3f6; padding: 16px; border-radius: 6px; overflow-x: auto;">
299
+ ${yamlAttachment}
300
+ </pre>`;
301
+ const result = `<h1>🚨 Issues Detected</h1>
302
+
303
+ <p>X-Fidelity found issues in your codebase:</p>
229
304
 
230
- ## Summary
231
- - Archetype: ${results.XFI_RESULT.archetype}
232
- - Total files analyzed: ${results.XFI_RESULT.fileCount}
233
- - Issues found: ${results.XFI_RESULT.totalIssues}
234
- - Warnings: ${results.XFI_RESULT.warningCount}
235
- - Errors: ${results.XFI_RESULT.errorCount}
236
- - Fatalities: ${results.XFI_RESULT.fatalityCount}
237
- - Exemptions: ${results.XFI_RESULT.exemptCount}
305
+ <h2>📊 Summary</h2>
306
+ <ul>
307
+ <li><strong>Archetype:</strong> <code>${results.XFI_RESULT.archetype}</code></li>
308
+ <li><strong>Files analyzed:</strong> ${results.XFI_RESULT.fileCount}</li>
309
+ <li><strong>Total issues:</strong> ${results.XFI_RESULT.totalIssues}</li>
310
+ <li>⚠️ Warnings: ${results.XFI_RESULT.warningCount}</li>
311
+ <li>❌ Errors: ${results.XFI_RESULT.errorCount}</li>
312
+ <li>🔥 Fatalities: ${results.XFI_RESULT.fatalityCount}</li>
313
+ </ul>
238
314
 
239
- ## Affected Files
240
- ${affectedFiles.map(file => `- ${file}`).join('\n')}
315
+ <h2>📝 Issues by File</h2>
316
+ <ul>
317
+ ${fileDetails}
318
+ </ul>
241
319
 
242
- ## Execution Time
243
- ${results.XFI_RESULT.durationSeconds} seconds
320
+ <p>Please address these issues as soon as possible.</p>
244
321
 
245
- For detailed information, please check the CI logs.
246
- `;
322
+ ${yamlSection}
323
+ `;
324
+ logger_1.logger.trace(result);
325
+ return result;
326
+ }
327
+ else {
328
+ // Success template
329
+ return `<h1>✅ Success!</h1>
330
+
331
+ <p>Your codebase passed all X-Fidelity checks.</p>
332
+
333
+ <h2>📊 Summary</h2>
334
+ <ul>
335
+ <li><strong>Archetype:</strong> <code>${results.XFI_RESULT.archetype}</code></li>
336
+ <li><strong>Files analyzed:</strong> ${results.XFI_RESULT.fileCount}</li>
337
+ <li><strong>Execution time:</strong> ${results.XFI_RESULT.durationSeconds} seconds</li>
338
+ </ul>
339
+
340
+ <p>🎉 Great job keeping the code clean!</p>`;
341
+ }
247
342
  }
248
343
  }
249
344
  exports.NotificationManager = NotificationManager;
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.EmailProvider = void 0;
16
16
  const logger_1 = require("../../utils/logger");
17
17
  const nodemailer_1 = __importDefault(require("nodemailer"));
18
+ const yaml_1 = require("yaml");
18
19
  class EmailProvider {
19
20
  constructor(config) {
20
21
  this.name = 'email';
@@ -22,6 +23,7 @@ class EmailProvider {
22
23
  }
23
24
  send(notification) {
24
25
  return __awaiter(this, void 0, void 0, function* () {
26
+ var _a;
25
27
  try {
26
28
  logger_1.logger.debug({
27
29
  config: Object.assign(Object.assign({}, this.config), { auth: {
@@ -52,11 +54,17 @@ class EmailProvider {
52
54
  logger_1.logger.error(verifyError, 'Failed to verify SMTP connection');
53
55
  return false;
54
56
  }
57
+ // Generate plain text version with YAML
58
+ const plainTextContent = `${notification.content.replace(/<[^>]*>/g, '')}
59
+
60
+ --- Full Results ---
61
+ ${(0, yaml_1.stringify)((_a = notification.metadata) === null || _a === void 0 ? void 0 : _a.results.XFI_RESULT)}`;
55
62
  const info = yield transporter.sendMail({
56
63
  from: this.config.from,
57
64
  to: notification.recipients.join(', '),
58
65
  subject: notification.subject,
59
- text: notification.content,
66
+ html: notification.content,
67
+ text: plainTextContent,
60
68
  });
61
69
  logger_1.logger.info({
62
70
  messageId: info.messageId,
@@ -13,7 +13,6 @@ export interface NotificationConfig {
13
13
  providers: string[];
14
14
  codeOwnersPath?: string;
15
15
  codeOwnersEnabled?: boolean;
16
- reportTemplate?: string;
17
16
  notifyOnSuccess?: boolean;
18
17
  notifyOnFailure?: boolean;
19
18
  }
@@ -256,10 +256,6 @@ export interface RepoXFIConfig {
256
256
  codeOwners?: boolean;
257
257
  notifyOnSuccess?: boolean;
258
258
  notifyOnFailure?: boolean;
259
- customTemplates?: {
260
- success?: string;
261
- failure?: string;
262
- };
263
259
  };
264
260
  [key: string]: any;
265
261
  }
package/dist/xfidelity CHANGED
@@ -123,14 +123,11 @@ function main() {
123
123
  // Send notifications if enabled
124
124
  if (notificationConfig.enabled) {
125
125
  logger_1.logger.debug('Notifications are enabled, preparing to send report');
126
- // Get list of affected files (those with issues)
127
- const affectedFiles = getAffectedFiles(resultMetadata);
128
126
  logger_1.logger.debug({
129
- affectedFilesCount: affectedFiles.length,
130
- hasRepoConfig: !!resultMetadata.XFI_RESULT.repoXFIConfig
127
+ affectedFilesCount: resultMetadata.XFI_RESULT.issueDetails.length
131
128
  }, 'Preparing notification data');
132
129
  // Pass the repo config to the notification manager
133
- yield notificationManager.sendReport(resultMetadata, affectedFiles, resultMetadata.XFI_RESULT.repoXFIConfig);
130
+ yield notificationManager.sendReport(resultMetadata);
134
131
  }
135
132
  // if results are found, there were issues found in the codebase
136
133
  if (resultMetadata.XFI_RESULT.totalIssues > 0) {
@@ -171,18 +168,6 @@ function main() {
171
168
  }
172
169
  });
173
170
  }
174
- // Helper function to extract affected files from results
175
- function getAffectedFiles(resultMetadata) {
176
- const affectedFiles = [];
177
- if (resultMetadata.XFI_RESULT.issueDetails) {
178
- for (const issue of resultMetadata.XFI_RESULT.issueDetails) {
179
- if (issue.filePath && !affectedFiles.includes(issue.filePath)) {
180
- affectedFiles.push(issue.filePath);
181
- }
182
- }
183
- }
184
- return affectedFiles;
185
- }
186
171
  var configManager_1 = require("./core/configManager");
187
172
  Object.defineProperty(exports, "repoDir", { enumerable: true, get: function () { return configManager_1.repoDir; } });
188
173
  __exportStar(require("./utils/logger"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-fidelity",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "cli for opinionated framework adherence checks",
5
5
  "main": "dist/index",
6
6
  "types": "dist/index.d.ts",
@@ -101,6 +101,7 @@
101
101
  "prettyjson": "^1.2.5",
102
102
  "sanitize-filename": "^1.6.3",
103
103
  "semver": "^7.7.1",
104
+ "yaml": "^2.7.0",
104
105
  "yarn": "^1.22.22"
105
106
  },
106
107
  "peerDependencies": {
@@ -12,10 +12,10 @@
12
12
  "fact": "globalFileAnalysis",
13
13
  "params": {
14
14
  "newPatterns": [
15
- "logger\\.info\\("
15
+ "const plugin: XFiPlugin = {"
16
16
  ],
17
17
  "legacyPatterns": [
18
- "logger\\.debug\\("
18
+ "import {.*FactDefn.*} from '\\.\\./types/typeDefs';"
19
19
  ],
20
20
  "fileFilter": ".*\\.(ts|js)$",
21
21
  "resultFact": "sdkUsageAnalysis"
@@ -11,7 +11,11 @@
11
11
  {
12
12
  "fact": "repoFileAnalysis",
13
13
  "params": {
14
- "checkPattern": ["oracle", "mysql", "mssql", "postgres", "sqlite", "mongodb", "cassandra", "redis", "rethinkdb", "neo4j", "couchdb"],
14
+ "checkPattern": [
15
+ "[\\s\\'\\\"\\.](oracle)[\\s\\'\\\"\\.]",
16
+ "[\\s\\'\\\"\\.](postgres)[\\s\\'\\\"\\.]",
17
+ "[\\s\\'\\\"\\.](mongodb)[\\s\\'\\\"\\.]"
18
+ ],
15
19
  "resultFact": "fileResultsDB"
16
20
  },
17
21
  "operator": "fileContains",
@@ -23,11 +23,9 @@
23
23
  "checkPattern": [
24
24
  "(api[_-]?key|auth[_-]?token|access[_-]?token|secret[_-]?key)",
25
25
  "(aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key)",
26
- "(password|passphrase)",
27
26
  "(private[_-]?key|ssh[_-]?key)",
28
27
  "(oauth[_-]?token|jwt[_-]?token)",
29
- "db[_-]?password",
30
- "(declare)"
28
+ "db[_-]?password"
31
29
  ],
32
30
  "resultFact": "fileResults"
33
31
  },
package/src/index.test.ts CHANGED
@@ -13,6 +13,7 @@ jest.mock('./utils/logger', () => ({
13
13
  warn: jest.fn(),
14
14
  error: jest.fn(),
15
15
  debug: jest.fn(),
16
+ trace: jest.fn()
16
17
  },
17
18
  setLogPrefix: jest.fn(),
18
19
  setLogLevel: jest.fn(),
package/src/index.ts CHANGED
@@ -81,18 +81,13 @@ export async function main() {
81
81
  // Send notifications if enabled
82
82
  if (notificationConfig.enabled) {
83
83
  logger.debug('Notifications are enabled, preparing to send report');
84
- // Get list of affected files (those with issues)
85
- const affectedFiles = getAffectedFiles(resultMetadata);
86
84
  logger.debug({
87
- affectedFilesCount: affectedFiles.length,
88
- hasRepoConfig: !!resultMetadata.XFI_RESULT.repoXFIConfig
85
+ affectedFilesCount: resultMetadata.XFI_RESULT.issueDetails.length
89
86
  }, 'Preparing notification data');
90
87
 
91
88
  // Pass the repo config to the notification manager
92
89
  await notificationManager.sendReport(
93
- resultMetadata,
94
- affectedFiles,
95
- resultMetadata.XFI_RESULT.repoXFIConfig
90
+ resultMetadata
96
91
  );
97
92
  }
98
93
 
@@ -135,21 +130,6 @@ export async function main() {
135
130
  }
136
131
  }
137
132
 
138
- // Helper function to extract affected files from results
139
- function getAffectedFiles(resultMetadata: ResultMetadata): string[] {
140
- const affectedFiles: string[] = [];
141
-
142
- if (resultMetadata.XFI_RESULT.issueDetails) {
143
- for (const issue of resultMetadata.XFI_RESULT.issueDetails) {
144
- if (issue.filePath && !affectedFiles.includes(issue.filePath)) {
145
- affectedFiles.push(issue.filePath);
146
- }
147
- }
148
- }
149
-
150
- return affectedFiles;
151
- }
152
-
153
133
  export { repoDir } from './core/configManager';
154
134
  export * from './utils/logger';
155
135
  export * from './types';
@@ -1,8 +1,9 @@
1
1
  import { logger } from '../utils/logger';
2
+ import { execSync } from 'child_process';
2
3
  import { ResultMetadata, RepoXFIConfig } from '../types/typeDefs';
3
4
  import { Notification, NotificationProvider, NotificationConfig, CodeOwner } from '../types/notificationTypes';
4
5
  import fs from 'fs';
5
- import path from 'path';
6
+ import { stringify as yamlStringify } from 'yaml';
6
7
 
7
8
  export class NotificationManager {
8
9
  private static instance: NotificationManager;
@@ -33,17 +34,17 @@ export class NotificationManager {
33
34
  logger.info(`Registered notification provider: ${provider.name}`);
34
35
  }
35
36
 
36
- public async sendReport(results: ResultMetadata, affectedFiles: string[], repoXFIConfig?: RepoXFIConfig): Promise<void> {
37
+ public async sendReport(results: ResultMetadata): Promise<void> {
37
38
  if (!this.config.enabled) {
38
39
  logger.debug('Notifications are disabled');
39
40
  return;
40
41
  }
41
42
 
42
43
  // Merge global config with repo-specific config
43
- const notifyConfig = this.mergeNotificationConfig(repoXFIConfig);
44
+ const notifyConfig = this.mergeNotificationConfig(results.XFI_RESULT.repoXFIConfig);
44
45
 
45
46
  // Determine if we should send notification based on results
46
- const hasIssues = results.XFI_RESULT.totalIssues > 0;
47
+ const hasIssues = results.XFI_RESULT.issueDetails.length > 0;
47
48
  if (hasIssues && !notifyConfig.notifyOnFailure) {
48
49
  logger.debug('Skipping notification for failure as notifyOnFailure is disabled');
49
50
  return;
@@ -54,24 +55,18 @@ export class NotificationManager {
54
55
  }
55
56
 
56
57
  // Get recipients from multiple sources
57
- const recipients = this.getRecipients(affectedFiles, repoXFIConfig, notifyConfig);
58
+ const recipients = this.getRecipients(this.getAffectedFiles(results), results.XFI_RESULT.repoXFIConfig, notifyConfig);
58
59
  if (Object.values(recipients).every(list => list.length === 0)) {
59
60
  logger.warn('No recipients found for notification');
60
61
  return;
61
62
  }
62
63
 
63
64
  // Generate report content
64
- const subject = hasIssues
65
+ const subject = hasIssues
65
66
  ? `[X-Fidelity] Issues found in your codebase (${results.XFI_RESULT.totalIssues})`
66
67
  : '[X-Fidelity] Your codebase passed all checks';
67
-
68
- // Use custom template if available
69
- const templateKey = hasIssues ? 'failure' : 'success';
70
- const customTemplate = repoXFIConfig?.notifications?.customTemplates?.[templateKey];
71
-
72
- const content = customTemplate
73
- ? this.applyTemplate(customTemplate, results, affectedFiles)
74
- : this.generateReportContent(results, affectedFiles);
68
+
69
+ const content = this.generateReportContent(results);
75
70
 
76
71
  // Send through all configured providers
77
72
  for (const providerName of this.config.providers) {
@@ -89,9 +84,9 @@ export class NotificationManager {
89
84
  recipients: providerRecipients,
90
85
  subject,
91
86
  content,
92
- metadata: {
87
+ metadata: {
93
88
  results,
94
- ciRunUrl: process.env.CI_RUN_URL || process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
89
+ ciRunUrl: process.env.CI_RUN_URL || process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
95
90
  ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
96
91
  : undefined
97
92
  }
@@ -115,22 +110,22 @@ export class NotificationManager {
115
110
 
116
111
  return {
117
112
  ...this.config,
118
- notifyOnSuccess: repoXFIConfig.notifications.notifyOnSuccess !== undefined
119
- ? repoXFIConfig.notifications.notifyOnSuccess
113
+ notifyOnSuccess: repoXFIConfig.notifications.notifyOnSuccess !== undefined
114
+ ? repoXFIConfig.notifications.notifyOnSuccess
120
115
  : this.config.notifyOnSuccess,
121
- notifyOnFailure: repoXFIConfig.notifications.notifyOnFailure !== undefined
122
- ? repoXFIConfig.notifications.notifyOnFailure
116
+ notifyOnFailure: repoXFIConfig.notifications.notifyOnFailure !== undefined
117
+ ? repoXFIConfig.notifications.notifyOnFailure
123
118
  : this.config.notifyOnFailure,
124
119
  // Use codeOwners setting from repo config if specified
125
- codeOwnersEnabled: repoXFIConfig.notifications.codeOwners !== undefined
126
- ? repoXFIConfig.notifications.codeOwners
120
+ codeOwnersEnabled: repoXFIConfig.notifications.codeOwners !== undefined
121
+ ? repoXFIConfig.notifications.codeOwners
127
122
  : this.config.codeOwnersEnabled
128
123
  };
129
124
  }
130
125
 
131
126
  private getRecipients(
132
- affectedFiles: string[],
133
- repoXFIConfig?: RepoXFIConfig,
127
+ affectedFiles: string[],
128
+ repoXFIConfig?: RepoXFIConfig,
134
129
  notifyConfig?: NotificationConfig
135
130
  ): Record<string, string[]> {
136
131
  const result: Record<string, string[]> = {
@@ -179,11 +174,11 @@ export class NotificationManager {
179
174
  try {
180
175
  const codeOwnersContent = fs.readFileSync(this.config.codeOwnersPath, 'utf8');
181
176
  const lines = codeOwnersContent.split('\n');
182
-
177
+
183
178
  for (const line of lines) {
184
179
  // Skip comments and empty lines
185
180
  if (line.trim().startsWith('#') || line.trim() === '') continue;
186
-
181
+
187
182
  const parts = line.trim().split(/\s+/);
188
183
  if (parts.length >= 2) {
189
184
  const path = parts[0];
@@ -191,11 +186,11 @@ export class NotificationManager {
191
186
  // Remove @ from GitHub usernames if present
192
187
  return owner.startsWith('@') ? owner.substring(1) : owner;
193
188
  });
194
-
189
+
195
190
  this.codeOwners.push({ path, owners });
196
191
  }
197
192
  }
198
-
193
+
199
194
  logger.info(`Loaded ${this.codeOwners.length} code owner entries`);
200
195
  } catch (error) {
201
196
  logger.error(error, `Failed to load CODEOWNERS file from ${this.config.codeOwnersPath}`);
@@ -204,7 +199,7 @@ export class NotificationManager {
204
199
 
205
200
  private getCodeOwnersForFiles(files: string[]): string[] {
206
201
  const owners = new Set<string>();
207
-
202
+
208
203
  for (const file of files) {
209
204
  for (const codeOwner of this.codeOwners) {
210
205
  // Simple glob matching (can be enhanced with proper glob matching)
@@ -213,7 +208,7 @@ export class NotificationManager {
213
208
  }
214
209
  }
215
210
  }
216
-
211
+
217
212
  return Array.from(owners);
218
213
  }
219
214
 
@@ -231,42 +226,156 @@ export class NotificationManager {
231
226
  return filePath === pattern;
232
227
  }
233
228
 
234
- private applyTemplate(template: string, results: ResultMetadata, affectedFiles: string[]): string {
235
- // Replace template variables with actual values
236
- return template
237
- .replace(/\${archetype}/g, results.XFI_RESULT.archetype)
238
- .replace(/\${fileCount}/g, String(results.XFI_RESULT.fileCount))
239
- .replace(/\${totalIssues}/g, String(results.XFI_RESULT.totalIssues))
240
- .replace(/\${warningCount}/g, String(results.XFI_RESULT.warningCount))
241
- .replace(/\${errorCount}/g, String(results.XFI_RESULT.errorCount))
242
- .replace(/\${fatalityCount}/g, String(results.XFI_RESULT.fatalityCount))
243
- .replace(/\${exemptCount}/g, String(results.XFI_RESULT.exemptCount))
244
- .replace(/\${affectedFiles}/g, affectedFiles.map(file => `- ${file}`).join('\n'))
245
- .replace(/\${date}/g, new Date().toISOString())
246
- .replace(/\${executionTime}/g, String(results.XFI_RESULT.durationSeconds));
229
+ // Helper function to extract affected files from results
230
+ private getAffectedFiles(results: ResultMetadata): string[] {
231
+ const affectedFiles: string[] = [];
232
+
233
+ if (results.XFI_RESULT.issueDetails) {
234
+ for (const issue of results.XFI_RESULT.issueDetails) {
235
+ if (issue.filePath && !affectedFiles.includes(issue.filePath)) {
236
+ affectedFiles.push(issue.filePath);
237
+ }
238
+ }
239
+ }
240
+
241
+ return affectedFiles;
247
242
  }
248
243
 
249
- private generateReportContent(results: ResultMetadata, affectedFiles: string[]): string {
250
- // Basic template - can be enhanced or made configurable
251
- return `
252
- # X-Fidelity Analysis Report
244
+ private generateReportContent(results: ResultMetadata): string {
245
+ // Get GitHub details from git commands with fallbacks to env vars
246
+ let githubServer = '';
247
+ let githubRepo = '';
248
+ let githubBranch = '';
249
+
250
+ try {
251
+ // Get remote URL and extract server/repo
252
+ const remoteUrl = execSync('git config --get remote.origin.url', { encoding: 'utf8', cwd: results.XFI_RESULT.repoPath })?.toString().trim();
253
+ if (remoteUrl) {
254
+ const match = remoteUrl.match(/^(?:https?:\/\/|git@)([^/:]+)[/:]([^/]+\/[^/.]+)(?:\.git)?$/);
255
+ if (match) {
256
+ githubServer = process.env.GITHUB_SERVER_URL || `https://${match[1]}`;
257
+ githubRepo = process.env.GITHUB_REPOSITORY || match[2];
258
+ }
259
+ }
260
+
261
+ // Get current branch
262
+ githubBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', cwd: results.XFI_RESULT.repoPath })?.toString().trim();
263
+ } catch (error) {
264
+ logger.warn('Failed to get git details, falling back to defaults', { error });
265
+ githubServer = process.env.GITHUB_SERVER_URL || 'https://github.com';
266
+ githubRepo = process.env.GITHUB_REPOSITORY || results.XFI_RESULT.repoUrl
267
+ .replace(/\.git$/, '')
268
+ .replace(/^git@([^:]+):/, 'https://$1/')
269
+ .replace(/^https?:\/\/[^\/]+\//, '');
270
+ githubBranch = process.env.GITHUB_REF_NAME || 'main';
271
+ }
272
+
273
+ const githubUrl = `${githubServer}/${githubRepo}/blob/${githubBranch}`;
274
+
275
+ let fileDetails = '';
276
+
277
+ if (results.XFI_RESULT.issueDetails.length > 0) {
253
278
 
254
- ## Summary
255
- - Archetype: ${results.XFI_RESULT.archetype}
256
- - Total files analyzed: ${results.XFI_RESULT.fileCount}
257
- - Issues found: ${results.XFI_RESULT.totalIssues}
258
- - Warnings: ${results.XFI_RESULT.warningCount}
259
- - Errors: ${results.XFI_RESULT.errorCount}
260
- - Fatalities: ${results.XFI_RESULT.fatalityCount}
261
- - Exemptions: ${results.XFI_RESULT.exemptCount}
279
+ try {
280
+ // Failure template
281
+ fileDetails = results.XFI_RESULT.issueDetails.map(issue => {
282
+ logger.debug(issue, `generating report content for issue: ${issue.filePath}`);
283
+ // Remove local path prefix from file paths
284
+ const relativePath = issue.filePath.replace(results.XFI_RESULT.repoPath + '/', '');
285
+ const fileIssues = issue.errors;
262
286
 
263
- ## Affected Files
264
- ${affectedFiles.map(file => `- ${file}`).join('\n')}
287
+ // Start with file name as bold list item with link
288
+ let output = `<li><strong><a href="${githubUrl}/${relativePath}">${relativePath}</a></strong><ul>`;
265
289
 
266
- ## Execution Time
267
- ${results.XFI_RESULT.durationSeconds} seconds
290
+ // Add rule failures with severity-based colors and line links
291
+ const ruleDetails = fileIssues?.map(error => {
292
+ const ruleName = error.ruleFailure;
293
+ const level = error.level;
294
+ const resultDetails = error?.details;
295
+ const message = resultDetails?.message;
268
296
 
269
- For detailed information, please check the CI logs.
270
- `;
297
+ let output = `<li>
298
+ <span>${ruleName}: ${level}</span><br>
299
+ <span>${message}</span><ul>
300
+ `;
301
+
302
+ if (!Array.isArray(resultDetails?.details || !resultDetails?.details?.details[0].lineNumber)) {
303
+ return '</li>' + output + '<pre>' + yamlStringify(resultDetails?.details) + '</pre></li></ul></li>';
304
+ }
305
+
306
+ const errorDetails = resultDetails?.details?.map((errorInstance: any) => {
307
+ const lineNumber = errorInstance?.lineNumber;
308
+ const lineLink = lineNumber ? `${githubUrl}/${relativePath}#L${lineNumber}` : `${githubUrl}/${relativePath}`;
309
+ const color = level === 'fatality' ? '#f20707' :
310
+ level === 'error' ? '#53046c' :
311
+ level === 'warning' ? '#944801' : '#1a1818';
312
+
313
+ return `<li><span style="color: ${color};font-weight:bold">${ruleName}</span>${lineNumber ? ` - <a href="${lineLink}">Line ${lineNumber}</a>` : ''}</li>`;
314
+ }).join('');
315
+
316
+ return output + errorDetails + '</ul></li>';
317
+ }).join('');
318
+
319
+ logger.debug(`fileDetails: ${output + ruleDetails}</ul></li>`);
320
+ return output + ruleDetails + '</ul></li>';
321
+ }).join('');
322
+
323
+ } catch (error: any) {
324
+ logger.error(error, 'Failed to generate report content');
325
+ logger.error(JSON.stringify(error))
326
+ logger.error(error.message)
327
+ logger.error(error.stack)
328
+ return '';
329
+ }
330
+
331
+
332
+ // Generate YAML attachment
333
+ const yamlAttachment = yamlStringify(results.XFI_RESULT);
334
+ const yamlSection = `
335
+ <h2>📎 Full Results (YAML)</h2>
336
+ <pre style="background-color: #d1e3f6; padding: 16px; border-radius: 6px; overflow-x: auto;">
337
+ ${yamlAttachment}
338
+ </pre>`;
339
+
340
+ const result = `<h1>🚨 Issues Detected</h1>
341
+
342
+ <p>X-Fidelity found issues in your codebase:</p>
343
+
344
+ <h2>📊 Summary</h2>
345
+ <ul>
346
+ <li><strong>Archetype:</strong> <code>${results.XFI_RESULT.archetype}</code></li>
347
+ <li><strong>Files analyzed:</strong> ${results.XFI_RESULT.fileCount}</li>
348
+ <li><strong>Total issues:</strong> ${results.XFI_RESULT.totalIssues}</li>
349
+ <li>⚠️ Warnings: ${results.XFI_RESULT.warningCount}</li>
350
+ <li>❌ Errors: ${results.XFI_RESULT.errorCount}</li>
351
+ <li>🔥 Fatalities: ${results.XFI_RESULT.fatalityCount}</li>
352
+ </ul>
353
+
354
+ <h2>📝 Issues by File</h2>
355
+ <ul>
356
+ ${fileDetails}
357
+ </ul>
358
+
359
+ <p>Please address these issues as soon as possible.</p>
360
+
361
+ ${yamlSection}
362
+ `
363
+ logger.trace(result)
364
+ return result
365
+ } else {
366
+ // Success template
367
+ return `<h1>✅ Success!</h1>
368
+
369
+ <p>Your codebase passed all X-Fidelity checks.</p>
370
+
371
+ <h2>📊 Summary</h2>
372
+ <ul>
373
+ <li><strong>Archetype:</strong> <code>${results.XFI_RESULT.archetype}</code></li>
374
+ <li><strong>Files analyzed:</strong> ${results.XFI_RESULT.fileCount}</li>
375
+ <li><strong>Execution time:</strong> ${results.XFI_RESULT.durationSeconds} seconds</li>
376
+ </ul>
377
+
378
+ <p>🎉 Great job keeping the code clean!</p>`;
379
+ }
271
380
  }
272
381
  }
@@ -1,6 +1,7 @@
1
1
  import { NotificationProvider, Notification } from '../../types/notificationTypes';
2
2
  import { logger } from '../../utils/logger';
3
3
  import nodemailer from 'nodemailer';
4
+ import { stringify as yamlStringify } from 'yaml';
4
5
 
5
6
  export interface EmailProviderConfig {
6
7
  host: string;
@@ -57,11 +58,18 @@ export class EmailProvider implements NotificationProvider {
57
58
  return false;
58
59
  }
59
60
 
61
+ // Generate plain text version with YAML
62
+ const plainTextContent = `${notification.content.replace(/<[^>]*>/g, '')}
63
+
64
+ --- Full Results ---
65
+ ${yamlStringify(notification.metadata?.results.XFI_RESULT)}`;
66
+
60
67
  const info = await transporter.sendMail({
61
68
  from: this.config.from,
62
69
  to: notification.recipients.join(', '),
63
70
  subject: notification.subject,
64
- text: notification.content,
71
+ html: notification.content,
72
+ text: plainTextContent,
65
73
  });
66
74
 
67
75
  logger.info({
@@ -15,7 +15,6 @@ export interface NotificationConfig {
15
15
  providers: string[];
16
16
  codeOwnersPath?: string;
17
17
  codeOwnersEnabled?: boolean;
18
- reportTemplate?: string;
19
18
  notifyOnSuccess?: boolean;
20
19
  notifyOnFailure?: boolean;
21
20
  }
@@ -289,10 +289,6 @@ export interface RepoXFIConfig {
289
289
  codeOwners?: boolean;
290
290
  notifyOnSuccess?: boolean;
291
291
  notifyOnFailure?: boolean;
292
- customTemplates?: {
293
- success?: string;
294
- failure?: string;
295
- };
296
292
  };
297
293
  [key: string]: any; // Allow for additional properties
298
294
  }