x-fidelity 2.10.0 → 2.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/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # [2.12.0](https://github.com/zotoio/x-fidelity/compare/v2.11.0...v2.12.0) (2024-08-25)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **analysis:** ensure long single-line files are catered for and npm namespaces ([509b4db](https://github.com/zotoio/x-fidelity/commit/509b4db1c3bb3f2cb487e043f5d23689bc8ed42d))
7
+ * Handle [@namespace](https://github.com/namespace) packages in dependency analysis ([b5314ac](https://github.com/zotoio/x-fidelity/commit/b5314ac2b67f1bf241cbad3229c0ce3498bd9175))
8
+ * Refactor repoFileAnalysis function to improve performance ([ae725bb](https://github.com/zotoio/x-fidelity/commit/ae725bbab307008424bb7494444e66eaca684aad))
9
+
10
+
11
+ ### Features
12
+
13
+ * Implement file content splitting for analysis ([4d5f049](https://github.com/zotoio/x-fidelity/commit/4d5f04938d7fd78425e79ac74143439be1552879))
14
+
15
+ # [2.11.0](https://github.com/zotoio/x-fidelity/compare/v2.10.0...v2.11.0) (2024-08-24)
16
+
17
+
18
+ ### Features
19
+
20
+ * Add documentation for custom operators in x-fidelity ([d015fff](https://github.com/zotoio/x-fidelity/commit/d015fff73189704486f2a51eb8a235f2c161152d))
21
+ * Add new operators section in README ([a306c4d](https://github.com/zotoio/x-fidelity/commit/a306c4db3b4043e8e8cf6fcd9bb78e2282c82ec9))
22
+ * Update README.md with new features and enhancements ([751b4ed](https://github.com/zotoio/x-fidelity/commit/751b4eddb5abafd7500f52ec403b5a42d7b0c447))
23
+
1
24
  # [2.10.0](https://github.com/zotoio/x-fidelity/compare/v2.9.0...v2.10.0) (2024-08-24)
2
25
 
3
26
 
package/README.md CHANGED
@@ -31,10 +31,6 @@ x-fidelity is an advanced CLI tool and paired config server designed to perform
31
31
  ```
32
32
  xfidelity .
33
33
  ```
34
- 4. For more options:
35
- ```
36
- xfidelity --help
37
- ```
38
34
 
39
35
  ## Table of Contents
40
36
 
@@ -303,7 +299,7 @@ x-fidelity uses archetypes to define project-specific configurations. Archetypes
303
299
  - Implementation of GitHub webhook to update local config
304
300
  - New cache routes: clearcache and viewcache
305
301
  - JSON schema validation for archetypes and rules
306
- - Input validation using Joi for URL parameters and telemetry data
302
+ - Input validation for URL parameters and telemetry data
307
303
  - Helmet middleware for improved security headers
308
304
  - Rate limiting on the Express server
309
305
  - Refactored ConfigManager with static methods and caching
@@ -312,9 +308,34 @@ x-fidelity uses archetypes to define project-specific configurations. Archetypes
312
308
  - Shared secret option for telemetry client and server
313
309
  - Improved dependency compatibility checks for npm and yarn
314
310
  - Enhanced error handling and logging
311
+ - Updated `fileContains` operator to support array of patterns
312
+ - New exemptions feature for temporarily waiving specific rules
313
+ - Improved OpenAI analysis with customizable prompts and severity thresholds
315
314
 
316
315
  For more detailed information on these features and how to use them, please refer to the respective sections in this README.
317
316
 
317
+ ### Exemptions
318
+
319
+ x-fidelity now supports exemptions, allowing you to temporarily waive specific rules for a given repository. This feature is useful when you need to make exceptions to your standard rules due to specific project requirements or during a transition period.
320
+
321
+ Exemptions are defined in JSON files and include:
322
+ - The repository URL
323
+ - The rule being exempted
324
+ - An expiration date
325
+ - A reason for the exemption
326
+
327
+ For more details on how to use and manage exemptions, see the [Exemptions](#exemptions) section.
328
+
329
+ ### OpenAI Integration Enhancements
330
+
331
+ The OpenAI integration has been expanded to allow for more customizable analysis:
332
+
333
+ - Custom prompts can now be defined in OpenAI-specific rules
334
+ - Severity thresholds can be set for OpenAI analysis results
335
+ - New `openaiAnalysisHighSeverity` operator for fine-grained control over AI-generated insights
336
+
337
+ For more information on leveraging these new OpenAI features, see the [OpenAI Integration](#openai-integration) section.
338
+
318
339
  ### Archetype Structure
319
340
 
320
341
  Archetypes specify:
@@ -606,7 +627,7 @@ You can create custom OpenAI rules to leverage AI-powered analysis for specific
606
627
 
607
628
  ```json
608
629
  {
609
- "name": "openai-custom-analysis",
630
+ "name": "openaiCustomAnalysis-global",
610
631
  "conditions": {
611
632
  "all": [
612
633
  {
@@ -630,7 +651,7 @@ You can create custom OpenAI rules to leverage AI-powered analysis for specific
630
651
  "type": "warning",
631
652
  "params": {
632
653
  "message": "Custom message for the warning",
633
- "results": {
654
+ "details": {
634
655
  "fact": "openaiCustomAnalysisResult"
635
656
  }
636
657
  }
@@ -639,7 +660,7 @@ You can create custom OpenAI rules to leverage AI-powered analysis for specific
639
660
  ```
640
661
 
641
662
  3. Customize the rule:
642
- - Set a unique `name` for your rule, ensuring it starts with 'openai'.
663
+ - Set a unique `name` for your rule, ensuring it starts with 'openai' and ends with '-global'.
643
664
  - Modify the `prompt` in the `params` section to specify what you want the AI to analyze.
644
665
  - Adjust the `value` in the `openaiAnalysisHighSeverity` operator to set the severity threshold (1-10).
645
666
  - Customize the `message` in the `event` params to describe the warning.
@@ -648,6 +669,85 @@ You can create custom OpenAI rules to leverage AI-powered analysis for specific
648
669
 
649
670
  This structure allows you to create custom AI-powered rules that can analyze your codebase for specific patterns, best practices, or potential issues. Remember to follow the naming convention to ensure proper handling of OpenAI rules in the system.
650
671
 
672
+ ## Custom Operators
673
+
674
+ x-fidelity includes several custom operators that can be used in your rules. Here's a brief overview of each:
675
+
676
+ ### `fileContains` Operator
677
+
678
+ The `fileContains` operator checks if a file contains specific patterns. It supports an array of patterns for flexible content matching.
679
+
680
+ Usage example:
681
+ ```json
682
+ {
683
+ "fact": "repoFileAnalysis",
684
+ "params": {
685
+ "checkPattern": [
686
+ "pattern1",
687
+ "pattern2",
688
+ "pattern3"
689
+ ],
690
+ "resultFact": "fileResults"
691
+ },
692
+ "operator": "fileContains",
693
+ "value": true
694
+ }
695
+ ```
696
+
697
+ This operator returns true if any of the patterns in the array are found in the file content.
698
+
699
+ ### `outdatedFramework` Operator
700
+
701
+ The `outdatedFramework` operator checks if the project is using outdated versions of dependencies. It compares the installed versions of dependencies against the minimum required versions specified in the archetype configuration.
702
+
703
+ Key features:
704
+ - Supports both npm and Yarn package managers
705
+ - Handles version ranges and specific versions
706
+ - Checks nested dependencies
707
+ - Returns `true` if any dependency is outdated, triggering a rule failure
708
+
709
+ Usage example in a rule:
710
+ ```json
711
+ {
712
+ "fact": "repoDependencyAnalysis",
713
+ "params": {
714
+ "resultFact": "repoDependencyResults"
715
+ },
716
+ "operator": "outdatedFramework",
717
+ "value": true
718
+ }
719
+ ```
720
+
721
+ ### `nonStandardDirectoryStructure` Operator
722
+
723
+ The `nonStandardDirectoryStructure` operator verifies if the project follows the standard directory structure defined in the archetype. It recursively checks the project's directory structure against the specified standard structure.
724
+
725
+ Key features:
726
+ - Performs a deep comparison of the directory structure
727
+ - Ignores files and focuses only on directory structure
728
+ - Returns `true` if the structure doesn't match, triggering a rule failure
729
+ - Only runs on the special `REPO_GLOBAL_CHECK` file to ensure a single, comprehensive check
730
+
731
+ Usage example in a rule:
732
+ ```json
733
+ {
734
+ "fact": "fileData",
735
+ "path": "$.filePath",
736
+ "operator": "nonStandardDirectoryStructure",
737
+ "value": {
738
+ "fact": "standardStructure"
739
+ }
740
+ }
741
+ ```
742
+
743
+ Both operators play crucial roles in maintaining project consistency and up-to-date dependencies, contributing to the overall quality and maintainability of the codebase.
744
+
745
+ ### `openaiAnalysisHighSeverity` Operator
746
+
747
+ The `openaiAnalysisHighSeverity` operator is used with OpenAI integration to identify high-severity issues in the AI-generated analysis.
748
+
749
+ For more detailed information on how to use these operators in your rules, please refer to the specific rule examples in the documentation.
750
+
651
751
  ## Best Practices
652
752
 
653
753
  1. **Version Control**: Keep your x-fidelity configurations (archetypes and rules) in version control.
@@ -21,6 +21,9 @@
21
21
  ],
22
22
  "config": {
23
23
  "minimumDependencyVersions": {
24
+ "@types/react": "^17.0.0",
25
+ "react": "^17.0.0",
26
+ "@yarnpkg/lockfile": "^1.2.0",
24
27
  "commander": "^2.0.0",
25
28
  "nodemon": "^3.9.0"
26
29
  },
@@ -40,6 +40,7 @@ exports.getDependencyVersionFacts = getDependencyVersionFacts;
40
40
  exports.findPropertiesInTree = findPropertiesInTree;
41
41
  exports.repoDependencyAnalysis = repoDependencyAnalysis;
42
42
  exports.semverValid = semverValid;
43
+ exports.normalizePackageName = normalizePackageName;
43
44
  const logger_1 = require("../utils/logger");
44
45
  const child_process_1 = require("child_process");
45
46
  const semver = __importStar(require("semver"));
@@ -105,6 +106,13 @@ function processYarnDependencies(yarnOutput) {
105
106
  return newDep;
106
107
  };
107
108
  const extractNameAndVersion = (nameAndVersion) => {
109
+ const lastAtIndex = nameAndVersion.lastIndexOf('@');
110
+ if (nameAndVersion.startsWith('@') && lastAtIndex > 0) {
111
+ return {
112
+ name: nameAndVersion.substring(0, lastAtIndex),
113
+ version: nameAndVersion.substring(lastAtIndex + 1)
114
+ };
115
+ }
108
116
  const parts = nameAndVersion.split('@');
109
117
  return { name: parts[0], version: parts[1] };
110
118
  };
@@ -160,8 +168,10 @@ function findPropertiesInTree(depGraph, minVersions) {
160
168
  logger_1.logger.debug(`depGraph: ${JSON.stringify(depGraph)}`);
161
169
  function walk(dep, parentName = '') {
162
170
  const fullName = parentName ? `${parentName}/${dep.name}` : dep.name;
163
- if (Object.keys(minVersions).includes(dep.name)) {
164
- results.push({ dep: fullName, ver: dep.version, min: minVersions[dep.name] });
171
+ if (Object.keys(minVersions).some(key => key === dep.name || `@${key}` === dep.name)) {
172
+ const minVersionKey = Object.keys(minVersions).find(key => key === dep.name || `@${key}` === dep.name);
173
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
174
+ results.push({ dep: fullName, ver: dep.version, min: minVersions[minVersionKey] });
165
175
  }
166
176
  if (dep.dependencies) {
167
177
  dep.dependencies.forEach(childDep => {
@@ -201,8 +211,11 @@ function repoDependencyAnalysis(params, almanac) {
201
211
  return result;
202
212
  });
203
213
  }
204
- function semverValid(required, installed) {
205
- if (!required || !installed) {
214
+ function semverValid(installed, required) {
215
+ // Remove potential @namespace from installed version
216
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
217
+ const installedVersion = installed.includes('@') ? installed.split('@').pop() : installed;
218
+ if (!required || !installedVersion) {
206
219
  return true;
207
220
  }
208
221
  // If 'installed' is a single version and 'required' is a range
@@ -227,3 +240,6 @@ function semverValid(required, installed) {
227
240
  }
228
241
  return false;
229
242
  }
243
+ function normalizePackageName(name) {
244
+ return name.startsWith('@') ? name : `@${name}`;
245
+ }
@@ -89,17 +89,49 @@ function repoFileAnalysis(params, almanac) {
89
89
  const analysis = [];
90
90
  const lines = fileContent.split('\n');
91
91
  logger_1.logger.debug(`lines: ${lines.length}`);
92
- let lineNumber = 0;
92
+ const processedLines = [];
93
+ let splitOccurred = false;
93
94
  for (const line of lines) {
94
- lineNumber++;
95
+ if (line.length > 200) {
96
+ let startIndex = 0;
97
+ while (startIndex < line.length) {
98
+ let endIndex = startIndex + 200;
99
+ if (endIndex < line.length) {
100
+ // Find the nearest space or end of token
101
+ while (endIndex > startIndex && !(/\s/.test(line[endIndex]) || /\W/.test(line[endIndex]))) {
102
+ endIndex--;
103
+ }
104
+ // If no suitable break point found, use the full 200 characters
105
+ if (endIndex === startIndex) {
106
+ endIndex = startIndex + 200;
107
+ }
108
+ }
109
+ else {
110
+ endIndex = line.length;
111
+ }
112
+ processedLines.push(line.substring(startIndex, endIndex));
113
+ startIndex = endIndex;
114
+ }
115
+ splitOccurred = true;
116
+ }
117
+ else {
118
+ processedLines.push(line);
119
+ }
120
+ }
121
+ if (lines.length === 1 || splitOccurred) {
122
+ logger_1.logger.debug(`File content was split for analysis`);
123
+ }
124
+ for (let i = 0; i < processedLines.length; i++) {
125
+ const line = processedLines[i];
95
126
  for (const pattern of checkPatterns) {
96
127
  const regex = new RegExp(pattern, 'g');
97
128
  if (regex.test(line)) {
98
- logger_1.logger.debug(`match on line ${lineNumber} for pattern ${pattern}`);
129
+ logger_1.logger.debug(`match on line ${i + 1} for pattern ${pattern}`);
99
130
  const match = {
100
131
  'match': pattern,
101
- 'lineNumber': lineNumber,
102
- 'line': line
132
+ 'lineNumber': i + 1,
133
+ 'line': line,
134
+ 'splitOccurred': splitOccurred
103
135
  };
104
136
  analysis.push(match);
105
137
  }
@@ -18,7 +18,8 @@ const archetypeRuleRoute_1 = require("./routes/archetypeRuleRoute");
18
18
  const telemetryRoute_1 = require("./routes/telemetryRoute");
19
19
  const clearCacheRoute_1 = require("./routes/clearCacheRoute");
20
20
  const viewCacheRoute_1 = require("./routes/viewCacheRoute");
21
- const githubWebhookRoute_1 = require("./routes/githubWebhookRoute");
21
+ const githubWebhookConfigUpdateRoute_1 = require("./routes/githubWebhookConfigUpdateRoute");
22
+ const githubWebhookPullRequestCheckRoute_1 = require("./routes/githubWebhookPullRequestCheckRoute");
22
23
  const exemptionsRoute_1 = require("./routes/exemptionsRoute");
23
24
  const validateUrlInput_1 = require("./middleware/validateUrlInput");
24
25
  const validateTelemetryData_1 = require("./middleware/validateTelemetryData");
@@ -57,7 +58,8 @@ app.post('/telemetry', checkSharedSecret, validateTelemetryData_1.validateTeleme
57
58
  app.post('/clearcache', checkSharedSecret, clearCacheRoute_1.clearCacheRoute);
58
59
  app.get('/viewcache', checkSharedSecret, viewCacheRoute_1.viewCacheRoute);
59
60
  app.get('/archetypes/:archetype/exemptions', checkSharedSecret, exemptionsRoute_1.exemptionsRoute);
60
- app.post('/github-webhook', validateGithubWebhook_1.validateGithubWebhook, githubWebhookRoute_1.githubWebhookRoute);
61
+ app.post('/github-config-update', validateGithubWebhook_1.validateGithubWebhook, githubWebhookConfigUpdateRoute_1.githubWebhookConfigUpdateRoute);
62
+ app.post('/github-pull-request-check', validateGithubWebhook_1.validateGithubWebhook, githubWebhookPullRequestCheckRoute_1.githubWebhookPullRequestCheckRoute);
61
63
  function startServer({ customPort, executionLogPrefix }) {
62
64
  const serverPort = customPort ? parseInt(customPort) : port;
63
65
  executionLogPrefix && (0, logger_1.setLogPrefix)(executionLogPrefix);
@@ -6,15 +6,16 @@ const archetypeRuleRoute_1 = require("./routes/archetypeRuleRoute");
6
6
  const telemetryRoute_1 = require("./routes/telemetryRoute");
7
7
  const clearCacheRoute_1 = require("./routes/clearCacheRoute");
8
8
  const viewCacheRoute_1 = require("./routes/viewCacheRoute");
9
- const githubWebhookRoute_1 = require("./routes/githubWebhookRoute");
9
+ const githubWebhookConfigUpdateRoute_1 = require("./routes/githubWebhookConfigUpdateRoute");
10
10
  const checkSharedSecret_1 = require("./middleware/checkSharedSecret");
11
+ const validateGithubWebhook_1 = require("./middleware/validateGithubWebhook");
11
12
  jest.mock('./routes/archetypeRoute');
12
13
  jest.mock('./routes/archetypeRulesRoute');
13
14
  jest.mock('./routes/archetypeRuleRoute');
14
15
  jest.mock('./routes/telemetryRoute');
15
16
  jest.mock('./routes/clearCacheRoute');
16
17
  jest.mock('./routes/viewCacheRoute');
17
- jest.mock('./routes/githubWebhookRoute');
18
+ jest.mock('./routes/githubWebhookConfigUpdateRoute');
18
19
  jest.mock('./middleware/checkSharedSecret');
19
20
  describe('configServer', () => {
20
21
  let app;
@@ -32,7 +33,7 @@ describe('configServer', () => {
32
33
  app.post('/telemetry', checkSharedSecret_1.checkSharedSecret, telemetryRoute_1.telemetryRoute);
33
34
  app.post('/clearcache', checkSharedSecret_1.checkSharedSecret, clearCacheRoute_1.clearCacheRoute);
34
35
  app.get('/viewcache', checkSharedSecret_1.checkSharedSecret, viewCacheRoute_1.viewCacheRoute);
35
- app.post('/github-webhook', githubWebhookRoute_1.githubWebhookRoute);
36
+ app.post('/github-config-update', validateGithubWebhook_1.validateGithubWebhook, githubWebhookConfigUpdateRoute_1.githubWebhookConfigUpdateRoute);
36
37
  // Verify routes are set up correctly
37
38
  expect(app.get).toHaveBeenCalledWith('/archetypes/:archetype', archetypeRoute_1.archetypeRoute);
38
39
  expect(app.get).toHaveBeenCalledWith('/archetypes/:archetype/rules', archetypeRulesRoute_1.archetypeRulesRoute);
@@ -40,7 +41,7 @@ describe('configServer', () => {
40
41
  expect(app.post).toHaveBeenCalledWith('/telemetry', checkSharedSecret_1.checkSharedSecret, telemetryRoute_1.telemetryRoute);
41
42
  expect(app.post).toHaveBeenCalledWith('/clearcache', checkSharedSecret_1.checkSharedSecret, clearCacheRoute_1.clearCacheRoute);
42
43
  expect(app.get).toHaveBeenCalledWith('/viewcache', checkSharedSecret_1.checkSharedSecret, viewCacheRoute_1.viewCacheRoute);
43
- expect(app.post).toHaveBeenCalledWith('/github-webhook', githubWebhookRoute_1.githubWebhookRoute);
44
+ expect(app.post).toHaveBeenCalledWith('/github-config-update', validateGithubWebhook_1.validateGithubWebhook, githubWebhookConfigUpdateRoute_1.githubWebhookConfigUpdateRoute);
44
45
  });
45
46
  afterEach(() => {
46
47
  jest.resetAllMocks();
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.githubWebhookRoute = githubWebhookRoute;
15
+ exports.githubWebhookConfigUpdateRoute = githubWebhookConfigUpdateRoute;
16
16
  const logger_1 = require("../../utils/logger");
17
17
  const crypto_1 = __importDefault(require("crypto"));
18
18
  const axios_1 = __importDefault(require("axios"));
@@ -22,7 +22,7 @@ const fs_1 = __importDefault(require("fs"));
22
22
  const cacheManager_1 = require("../cacheManager");
23
23
  const configManager_1 = require("../../utils/configManager");
24
24
  const cli_1 = require("../../core/cli");
25
- function githubWebhookRoute(req, res) {
25
+ function githubWebhookConfigUpdateRoute(req, res) {
26
26
  return __awaiter(this, void 0, void 0, function* () {
27
27
  const requestLogPrefix = req.headers['x-log-prefix'] || '';
28
28
  (0, logger_1.setLogPrefix)(requestLogPrefix);
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.githubWebhookPullRequestCheckRoute = githubWebhookPullRequestCheckRoute;
16
+ const logger_1 = require("../../utils/logger");
17
+ const crypto_1 = __importDefault(require("crypto"));
18
+ function githubWebhookPullRequestCheckRoute(req, res) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ const requestLogPrefix = req.headers['x-log-prefix'] || '';
21
+ (0, logger_1.setLogPrefix)(requestLogPrefix);
22
+ const signature = req.headers['x-hub-signature-256'];
23
+ const githubSecret = process.env.GITHUB_WEBHOOK_SECRET;
24
+ if (!githubSecret) {
25
+ logger_1.logger.error('GitHub webhook secret is not set');
26
+ return res.status(500).send('Server is not configured for webhooks');
27
+ }
28
+ if (!signature) {
29
+ logger_1.logger.error('No X-Hub-Signature-256 found on request');
30
+ return res.status(400).send('No X-Hub-Signature-256 found on request');
31
+ }
32
+ const hmac = crypto_1.default.createHmac('sha256', githubSecret);
33
+ const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');
34
+ if (signature !== digest) {
35
+ logger_1.logger.error('Request body digest did not match X-Hub-Signature-256');
36
+ return res.status(400).send('Invalid signature');
37
+ }
38
+ const event = req.headers['x-github-event'];
39
+ if (event === 'push') {
40
+ // TODO: Implement pull request check
41
+ return res.status(200).send('Webhook received and processed');
42
+ }
43
+ res.status(200).send('Received');
44
+ });
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-fidelity",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "cli for opinionated framework adherence checks",
5
5
  "main": "dist/xfidelity",
6
6
  "bin": {
@@ -21,6 +21,9 @@
21
21
  ],
22
22
  "config": {
23
23
  "minimumDependencyVersions": {
24
+ "@types/react": "^17.0.0",
25
+ "react": "^17.0.0",
26
+ "@yarnpkg/lockfile": "^1.2.0",
24
27
  "commander": "^2.0.0",
25
28
  "nodemon": "^3.9.0"
26
29
  },
@@ -64,9 +64,16 @@ function processYarnDependencies(yarnOutput: any): LocalDependencies[] {
64
64
  return newDep;
65
65
  };
66
66
  const extractNameAndVersion = (nameAndVersion: string) => {
67
+ const lastAtIndex = nameAndVersion.lastIndexOf('@');
68
+ if (nameAndVersion.startsWith('@') && lastAtIndex > 0) {
69
+ return {
70
+ name: nameAndVersion.substring(0, lastAtIndex),
71
+ version: nameAndVersion.substring(lastAtIndex + 1)
72
+ };
73
+ }
67
74
  const parts = nameAndVersion.split('@');
68
75
  return { name: parts[0], version: parts[1] };
69
- }
76
+ };
70
77
  yarnOutput.data.trees.forEach((tree: any) => {
71
78
  dependencies.push(processDependency(tree));
72
79
  });
@@ -125,8 +132,10 @@ export function findPropertiesInTree(depGraph: LocalDependencies[], minVersions:
125
132
 
126
133
  function walk(dep: LocalDependencies, parentName = '') {
127
134
  const fullName = parentName ? `${parentName}/${dep.name}` : dep.name;
128
- if (Object.keys(minVersions).includes(dep.name)) {
129
- results.push({ dep: fullName, ver: dep.version, min: minVersions[dep.name] });
135
+ if (Object.keys(minVersions).some(key => key === dep.name || `@${key}` === dep.name)) {
136
+ const minVersionKey = Object.keys(minVersions).find(key => key === dep.name || `@${key}` === dep.name);
137
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
138
+ results.push({ dep: fullName, ver: dep.version, min: minVersions[minVersionKey!] });
130
139
  }
131
140
  if (dep.dependencies) {
132
141
  dep.dependencies.forEach(childDep => {
@@ -176,9 +185,12 @@ export async function repoDependencyAnalysis(params: any, almanac: Almanac) {
176
185
  return result;
177
186
  }
178
187
 
179
- export function semverValid(required: string, installed: string): boolean {
180
-
181
- if (!required || !installed) {
188
+ export function semverValid(installed: string, required: string): boolean {
189
+ // Remove potential @namespace from installed version
190
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191
+ const installedVersion = installed.includes('@') ? installed.split('@').pop()! : installed;
192
+
193
+ if (!required || !installedVersion) {
182
194
  return true;
183
195
  }
184
196
 
@@ -208,3 +220,6 @@ export function semverValid(required: string, installed: string): boolean {
208
220
 
209
221
  return false;
210
222
  }
223
+ export function normalizePackageName(name: string): string {
224
+ return name.startsWith('@') ? name : `@${name}`;
225
+ }
@@ -83,17 +83,50 @@ async function repoFileAnalysis(params: any, almanac: any) {
83
83
  const analysis: any = [];
84
84
  const lines = fileContent.split('\n');
85
85
  logger.debug(`lines: ${lines.length}`);
86
- let lineNumber = 0;
86
+ const processedLines: string[] = [];
87
+ let splitOccurred = false;
88
+
87
89
  for (const line of lines) {
88
- lineNumber++;
90
+ if (line.length > 200) {
91
+ let startIndex = 0;
92
+ while (startIndex < line.length) {
93
+ let endIndex = startIndex + 200;
94
+ if (endIndex < line.length) {
95
+ // Find the nearest space or end of token
96
+ while (endIndex > startIndex && !(/\s/.test(line[endIndex]) || /\W/.test(line[endIndex]))) {
97
+ endIndex--;
98
+ }
99
+ // If no suitable break point found, use the full 200 characters
100
+ if (endIndex === startIndex) {
101
+ endIndex = startIndex + 200;
102
+ }
103
+ } else {
104
+ endIndex = line.length;
105
+ }
106
+ processedLines.push(line.substring(startIndex, endIndex));
107
+ startIndex = endIndex;
108
+ }
109
+ splitOccurred = true;
110
+ } else {
111
+ processedLines.push(line);
112
+ }
113
+ }
114
+
115
+ if (lines.length === 1 || splitOccurred) {
116
+ logger.debug(`File content was split for analysis`);
117
+ }
118
+
119
+ for (let i = 0; i < processedLines.length; i++) {
120
+ const line = processedLines[i];
89
121
  for (const pattern of checkPatterns) {
90
122
  const regex = new RegExp(pattern, 'g');
91
123
  if (regex.test(line)) {
92
- logger.debug(`match on line ${lineNumber} for pattern ${pattern}`)
124
+ logger.debug(`match on line ${i + 1} for pattern ${pattern}`);
93
125
  const match = {
94
126
  'match': pattern,
95
- 'lineNumber': lineNumber,
96
- 'line': line
127
+ 'lineNumber': i + 1,
128
+ 'line': line,
129
+ 'splitOccurred': splitOccurred
97
130
  };
98
131
  analysis.push(match);
99
132
  }
@@ -5,8 +5,9 @@ import { archetypeRuleRoute } from './routes/archetypeRuleRoute';
5
5
  import { telemetryRoute } from './routes/telemetryRoute';
6
6
  import { clearCacheRoute } from './routes/clearCacheRoute';
7
7
  import { viewCacheRoute } from './routes/viewCacheRoute';
8
- import { githubWebhookRoute } from './routes/githubWebhookRoute';
8
+ import { githubWebhookConfigUpdateRoute } from './routes/githubWebhookConfigUpdateRoute';
9
9
  import { checkSharedSecret } from './middleware/checkSharedSecret';
10
+ import { validateGithubWebhook } from './middleware/validateGithubWebhook';
10
11
 
11
12
  jest.mock('./routes/archetypeRoute');
12
13
  jest.mock('./routes/archetypeRulesRoute');
@@ -14,7 +15,7 @@ jest.mock('./routes/archetypeRuleRoute');
14
15
  jest.mock('./routes/telemetryRoute');
15
16
  jest.mock('./routes/clearCacheRoute');
16
17
  jest.mock('./routes/viewCacheRoute');
17
- jest.mock('./routes/githubWebhookRoute');
18
+ jest.mock('./routes/githubWebhookConfigUpdateRoute');
18
19
  jest.mock('./middleware/checkSharedSecret');
19
20
 
20
21
  describe('configServer', () => {
@@ -35,7 +36,7 @@ describe('configServer', () => {
35
36
  app.post('/telemetry', checkSharedSecret, telemetryRoute);
36
37
  app.post('/clearcache', checkSharedSecret, clearCacheRoute);
37
38
  app.get('/viewcache', checkSharedSecret, viewCacheRoute);
38
- app.post('/github-webhook', githubWebhookRoute);
39
+ app.post('/github-config-update', validateGithubWebhook, githubWebhookConfigUpdateRoute);
39
40
 
40
41
  // Verify routes are set up correctly
41
42
  expect(app.get).toHaveBeenCalledWith('/archetypes/:archetype', archetypeRoute);
@@ -44,7 +45,7 @@ describe('configServer', () => {
44
45
  expect(app.post).toHaveBeenCalledWith('/telemetry', checkSharedSecret, telemetryRoute);
45
46
  expect(app.post).toHaveBeenCalledWith('/clearcache', checkSharedSecret, clearCacheRoute);
46
47
  expect(app.get).toHaveBeenCalledWith('/viewcache', checkSharedSecret, viewCacheRoute);
47
- expect(app.post).toHaveBeenCalledWith('/github-webhook', githubWebhookRoute);
48
+ expect(app.post).toHaveBeenCalledWith('/github-config-update', validateGithubWebhook, githubWebhookConfigUpdateRoute);
48
49
  });
49
50
 
50
51
  afterEach(() => {
@@ -13,7 +13,8 @@ import { archetypeRuleRoute } from './routes/archetypeRuleRoute';
13
13
  import { telemetryRoute } from './routes/telemetryRoute';
14
14
  import { clearCacheRoute } from './routes/clearCacheRoute';
15
15
  import { viewCacheRoute } from './routes/viewCacheRoute';
16
- import { githubWebhookRoute } from './routes/githubWebhookRoute';
16
+ import { githubWebhookConfigUpdateRoute } from './routes/githubWebhookConfigUpdateRoute';
17
+ import { githubWebhookPullRequestCheckRoute } from './routes/githubWebhookPullRequestCheckRoute';
17
18
  import { exemptionsRoute } from './routes/exemptionsRoute';
18
19
  import { validateUrlInput } from './middleware/validateUrlInput';
19
20
  import { validateTelemetryData } from './middleware/validateTelemetryData';
@@ -62,7 +63,8 @@ app.post('/clearcache', checkSharedSecret, clearCacheRoute);
62
63
  app.get('/viewcache', checkSharedSecret, viewCacheRoute);
63
64
  app.get('/archetypes/:archetype/exemptions', checkSharedSecret, exemptionsRoute);
64
65
 
65
- app.post('/github-webhook', validateGithubWebhook, githubWebhookRoute);
66
+ app.post('/github-config-update', validateGithubWebhook, githubWebhookConfigUpdateRoute);
67
+ app.post('/github-pull-request-check', validateGithubWebhook, githubWebhookPullRequestCheckRoute);
66
68
 
67
69
  export function startServer({ customPort, executionLogPrefix }: StartServerParams): any {
68
70
  const serverPort = customPort ? parseInt(customPort) : port;
@@ -9,7 +9,7 @@ import { clearCache } from '../cacheManager';
9
9
  import { ConfigManager } from '../../utils/configManager';
10
10
  import { options } from '../../core/cli';
11
11
 
12
- export async function githubWebhookRoute(req: Request, res: Response) {
12
+ export async function githubWebhookConfigUpdateRoute(req: Request, res: Response) {
13
13
  const requestLogPrefix = req.headers['x-log-prefix'] as string || '';
14
14
  setLogPrefix(requestLogPrefix);
15
15
 
@@ -0,0 +1,40 @@
1
+ import { Request, Response } from 'express';
2
+ import { logger, setLogPrefix } from '../../utils/logger';
3
+ import crypto from 'crypto';
4
+
5
+ export async function githubWebhookPullRequestCheckRoute(req: Request, res: Response) {
6
+ const requestLogPrefix = req.headers['x-log-prefix'] as string || '';
7
+ setLogPrefix(requestLogPrefix);
8
+
9
+ const signature = req.headers['x-hub-signature-256'] as string;
10
+ const githubSecret = process.env.GITHUB_WEBHOOK_SECRET;
11
+
12
+ if (!githubSecret) {
13
+ logger.error('GitHub webhook secret is not set');
14
+ return res.status(500).send('Server is not configured for webhooks');
15
+ }
16
+
17
+ if (!signature) {
18
+ logger.error('No X-Hub-Signature-256 found on request');
19
+ return res.status(400).send('No X-Hub-Signature-256 found on request');
20
+ }
21
+
22
+ const hmac = crypto.createHmac('sha256', githubSecret);
23
+ const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');
24
+
25
+ if (signature !== digest) {
26
+ logger.error('Request body digest did not match X-Hub-Signature-256');
27
+ return res.status(400).send('Invalid signature');
28
+ }
29
+
30
+ const event = req.headers['x-github-event'] as string;
31
+ if (event === 'push') {
32
+ // TODO: Implement pull request check
33
+
34
+ return res.status(200).send('Webhook received and processed');
35
+ }
36
+
37
+ res.status(200).send('Received');
38
+ }
39
+
40
+