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 +23 -0
- package/README.md +108 -8
- package/dist/archetypes/node-fullstack.json +3 -0
- package/dist/facts/repoDependencyFacts.js +20 -4
- package/dist/facts/repoFilesystemFacts.js +37 -5
- package/dist/server/configServer.js +4 -2
- package/dist/server/configServer.test.js +5 -4
- package/dist/server/routes/{githubWebhookRoute.js → githubWebhookConfigUpdateRoute.js} +2 -2
- package/dist/server/routes/githubWebhookPullRequestCheckRoute.js +45 -0
- package/package.json +1 -1
- package/src/archetypes/node-fullstack.json +3 -0
- package/src/facts/repoDependencyFacts.ts +21 -6
- package/src/facts/repoFilesystemFacts.ts +38 -5
- package/src/server/configServer.test.ts +5 -4
- package/src/server/configServer.ts +4 -2
- package/src/server/routes/{githubWebhookRoute.ts → githubWebhookConfigUpdateRoute.ts} +1 -1
- package/src/server/routes/githubWebhookPullRequestCheckRoute.ts +40 -0
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
|
|
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": "
|
|
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
|
-
"
|
|
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.
|
|
@@ -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).
|
|
164
|
-
|
|
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(
|
|
205
|
-
|
|
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
|
-
|
|
92
|
+
const processedLines = [];
|
|
93
|
+
let splitOccurred = false;
|
|
93
94
|
for (const line of lines) {
|
|
94
|
-
|
|
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 ${
|
|
129
|
+
logger_1.logger.debug(`match on line ${i + 1} for pattern ${pattern}`);
|
|
99
130
|
const match = {
|
|
100
131
|
'match': pattern,
|
|
101
|
-
'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
|
|
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-
|
|
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
|
|
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/
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
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
|
@@ -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).
|
|
129
|
-
|
|
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(
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
86
|
+
const processedLines: string[] = [];
|
|
87
|
+
let splitOccurred = false;
|
|
88
|
+
|
|
87
89
|
for (const line of lines) {
|
|
88
|
-
|
|
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 ${
|
|
124
|
+
logger.debug(`match on line ${i + 1} for pattern ${pattern}`);
|
|
93
125
|
const match = {
|
|
94
126
|
'match': pattern,
|
|
95
|
-
'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 {
|
|
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/
|
|
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-
|
|
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-
|
|
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 {
|
|
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-
|
|
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
|
|
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
|
+
|