x-fidelity 2.12.1 → 2.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## [2.13.1](https://github.com/zotoio/x-fidelity/compare/v2.13.0...v2.13.1) (2024-08-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **schema:** relax for fix ([ee5e123](https://github.com/zotoio/x-fidelity/commit/ee5e12350cef95dcdcbe3e4930b1ca6f5a9b4609))
7
+
8
+ # [2.13.0](https://github.com/zotoio/x-fidelity/compare/v2.12.1...v2.13.0) (2024-08-28)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add readonly constraint to archetypeSchema ([c74d116](https://github.com/zotoio/x-fidelity/commit/c74d1168733795aac523ba8ceb33020f2f41911e))
14
+ * Improve path traversal prevention in repoFilesystemFacts.ts ([f65f595](https://github.com/zotoio/x-fidelity/commit/f65f59532683e7a8e392e8bcd259869b8fe2f006))
15
+ * **schema:** fix schema validation and incorrect default rule code in setupEngine ([0a3c65c](https://github.com/zotoio/x-fidelity/commit/0a3c65ce5d0873ec1b3e679ac343ae91e6c0e463))
16
+ * update import statement for RuleConfigSchema ([a658127](https://github.com/zotoio/x-fidelity/commit/a6581272708bcf77a37040c1fb0a6d0411d29a36))
17
+
18
+
19
+ ### Features
20
+
21
+ * Add new types for IsBlacklistedParams and isWhitelistedParams ([2c43c82](https://github.com/zotoio/x-fidelity/commit/2c43c822f5dbeeb8f10fcf157886fe607e451fe3))
22
+ * Update archetype typedef and jsonschema to validate semver strings ([2bba2b1](https://github.com/zotoio/x-fidelity/commit/2bba2b15a66f0937609b04815b029ff181f86ccf))
23
+
1
24
  ## [2.12.1](https://github.com/zotoio/x-fidelity/compare/v2.12.0...v2.12.1) (2024-08-25)
2
25
 
3
26
 
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "config": {
19
19
  "minimumDependencyVersions": {
20
- "spring-boot-starter": "^2.5.0",
20
+ "springbootstarter": ">2.5.0",
21
21
  "spring-boot-starter-web": "^2.5.0"
22
22
  },
23
23
  "standardStructure": {
@@ -21,11 +21,12 @@
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",
27
- "commander": "^2.0.0",
28
- "nodemon": "^3.9.0"
24
+ "@types/react": ">=18.0.0",
25
+ "react": "~17.0.0",
26
+ "@yarnpkg/lockfile": "<1.2.0",
27
+ "commander": ">=2.0.0 <13.0.0",
28
+ "nodemon": ">4.9.0",
29
+ "@colors/colors": "1.7.0 || 1.6.0"
29
30
  },
30
31
  "standardStructure": {
31
32
  "app": {
@@ -34,7 +34,7 @@ function setupEngine(params) {
34
34
  // Add rules to engine
35
35
  logger_1.logger.info(`=== loading json rules..`);
36
36
  const config = yield configManager_1.ConfigManager.getConfig({ archetype, logPrefix: executionLogPrefix });
37
- logger_1.logger.debug(config.rules);
37
+ logger_1.logger.debug(`rules loaded: ${config.rules}`);
38
38
  const addedRules = new Set();
39
39
  config.rules.forEach((rule) => {
40
40
  try {
@@ -61,19 +61,6 @@ function setupEngine(params) {
61
61
  logger_1.logger.error(e.message);
62
62
  }
63
63
  });
64
- if (addedRules.size === 0) {
65
- logger_1.logger.info('No valid rules were added. Adding default rules.');
66
- engine.addRule({
67
- name: 'default-rule-1',
68
- conditions: { all: [] },
69
- event: { type: 'warning', params: { message: 'Default rule 1 triggered' } }
70
- });
71
- engine.addRule({
72
- name: 'default-rule-2',
73
- conditions: { all: [] },
74
- event: { type: 'warning', params: { message: 'Default rule 2 triggered' } }
75
- });
76
- }
77
64
  engine.on('success', (_a) => __awaiter(this, [_a], void 0, function* ({ type, params }) {
78
65
  if (type === 'warning') {
79
66
  logger_1.logger.warn(`warning detected: ${JSON.stringify(params)}`);
@@ -83,31 +83,6 @@ describe('setupEngine', () => {
83
83
  expect(mockOn).toHaveBeenCalledTimes(1);
84
84
  expect(engine).toBeDefined();
85
85
  }));
86
- it('should handle errors when loading rules and add default rules', () => __awaiter(void 0, void 0, void 0, function* () {
87
- const mockAddRule = jest.fn();
88
- json_rules_engine_1.Engine.mockImplementation(() => ({
89
- addOperator: jest.fn(),
90
- addRule: mockAddRule,
91
- addFact: jest.fn(),
92
- on: jest.fn()
93
- }));
94
- configManager_1.ConfigManager.getConfig.mockResolvedValue({
95
- archetype: mockArchetypeConfig,
96
- rules: [
97
- undefined,
98
- { name: undefined },
99
- { name: 'invalidRule', conditions: null }
100
- ],
101
- cliOptions: {}
102
- });
103
- yield (0, engineSetup_1.setupEngine)(mockParams);
104
- expect(logger_1.logger.error).toHaveBeenCalledWith('Invalid rule configuration: rule or rule name is undefined');
105
- expect(logger_1.logger.error).toHaveBeenCalledWith('Invalid rule configuration: rule or rule name is undefined');
106
- expect(logger_1.logger.error).toHaveBeenCalledWith('Error loading rule: invalidRule');
107
- expect(logger_1.logger.info).toHaveBeenCalledWith('No valid rules were added. Adding default rules.');
108
- expect(mockAddRule).toHaveBeenCalledWith(expect.objectContaining({ name: 'default-rule-1' }));
109
- expect(mockAddRule).toHaveBeenCalledWith(expect.objectContaining({ name: 'default-rule-2' }));
110
- }));
111
86
  it('should set up event listeners for success events', () => __awaiter(void 0, void 0, void 0, function* () {
112
87
  const mockOn = jest.fn();
113
88
  json_rules_engine_1.Engine.mockImplementation(() => ({
@@ -214,28 +214,37 @@ function repoDependencyAnalysis(params, almanac) {
214
214
  function semverValid(installed, required) {
215
215
  // Remove potential @namespace from installed version
216
216
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
217
- const installedVersion = installed.includes('@') ? installed.split('@').pop() : installed;
218
- if (!required || !installedVersion) {
217
+ installed = installed.includes('@') ? installed.split('@').pop() : installed;
218
+ if (!installed || !required) {
219
219
  return true;
220
220
  }
221
+ // ensure that both inputs are now valid semver or range strings
222
+ if (!semver.valid(installed) && !semver.validRange(installed)) {
223
+ logger_1.logger.error(`semverValid: invalid installed version or range: ${installed}`);
224
+ return false;
225
+ }
226
+ if (!semver.valid(required) && !semver.validRange(required)) {
227
+ logger_1.logger.error(`semverValid: invalid required version or range: ${required}`);
228
+ return false;
229
+ }
221
230
  // If 'installed' is a single version and 'required' is a range
222
231
  if (semver.valid(installed) && semver.validRange(required)) {
223
- logger_1.logger.debug('range vs version');
232
+ logger_1.logger.info('range vs version');
224
233
  return semver.satisfies(installed, required);
225
234
  }
226
235
  // If 'required' is a single version and 'installed' is a range
227
236
  if (semver.valid(required) && semver.validRange(installed)) {
228
- logger_1.logger.debug('version vs range');
237
+ logger_1.logger.info('version vs range');
229
238
  return semver.satisfies(required, installed);
230
239
  }
231
240
  // If both are single versions, simply compare them
232
241
  if (semver.valid(required) && semver.valid(installed)) {
233
- logger_1.logger.debug('version vs version');
242
+ logger_1.logger.info('version vs version');
234
243
  return semver.gt(installed, required);
235
244
  }
236
245
  // If both are ranges, check if they intersect
237
246
  if (semver.validRange(required) && semver.validRange(installed)) {
238
- logger_1.logger.debug('range vs range');
247
+ logger_1.logger.info('range vs range');
239
248
  return semver.intersects(required, installed);
240
249
  }
241
250
  return false;
@@ -38,6 +38,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
38
  const repoDependencyFacts = __importStar(require("./repoDependencyFacts"));
39
39
  const child_process_1 = require("child_process");
40
40
  const fs_1 = __importDefault(require("fs"));
41
+ const repoDependencyFacts_1 = require("./repoDependencyFacts");
41
42
  jest.mock('child_process');
42
43
  jest.mock('fs');
43
44
  jest.mock('../utils/logger');
@@ -142,28 +143,70 @@ describe('repoDependencyFacts', () => {
142
143
  });
143
144
  describe('semverValid', () => {
144
145
  it('should return true for valid version comparisons', () => {
145
- expect(repoDependencyFacts.semverValid('2.0.0', '^1.0.0')).toBe(false);
146
- expect(repoDependencyFacts.semverValid('1.5.0', '1.0.0 - 2.0.0')).toBe(true);
147
- expect(repoDependencyFacts.semverValid('1.0.0', '1.0.0')).toBe(true);
148
- expect(repoDependencyFacts.semverValid('2.0.0', '>=1.0.0')).toBe(true);
146
+ expect((0, repoDependencyFacts_1.semverValid)('2.0.0', '^1.0.0')).toBe(false);
147
+ expect((0, repoDependencyFacts_1.semverValid)('1.5.0', '1.0.0 - 2.0.0')).toBe(true);
148
+ expect((0, repoDependencyFacts_1.semverValid)('1.0.0', '1.0.0')).toBe(true);
149
+ expect((0, repoDependencyFacts_1.semverValid)('2.0.0', '>=1.0.0')).toBe(true);
149
150
  });
150
151
  it('should return false for invalid version comparisons', () => {
151
- expect(repoDependencyFacts.semverValid('1.0.0', '^2.0.0')).toBe(false);
152
- expect(repoDependencyFacts.semverValid('3.0.0', '1.0.0 - 2.0.0')).toBe(false);
153
- expect(repoDependencyFacts.semverValid('0.9.0', '>=1.0.0')).toBe(false);
152
+ expect((0, repoDependencyFacts_1.semverValid)('1.0.0', '^2.0.0')).toBe(false);
153
+ expect((0, repoDependencyFacts_1.semverValid)('3.0.0', '1.0.0 - 2.0.0')).toBe(false);
154
+ expect((0, repoDependencyFacts_1.semverValid)('0.9.0', '>=1.0.0')).toBe(false);
154
155
  });
155
156
  it('should handle complex version ranges', () => {
156
- expect(repoDependencyFacts.semverValid('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
157
- expect(repoDependencyFacts.semverValid('2.5.0', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
158
- expect(repoDependencyFacts.semverValid('5.5.5', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
159
- expect(repoDependencyFacts.semverValid('8.0.0', '1.x || >=9.5.0 || 5.0.0 - 7.2.3')).toBe(false);
157
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
158
+ expect((0, repoDependencyFacts_1.semverValid)('2.5.0', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
159
+ expect((0, repoDependencyFacts_1.semverValid)('5.5.5', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
160
+ expect((0, repoDependencyFacts_1.semverValid)('8.0.0', '1.x || >=9.5.0 || 5.0.0 - 7.2.3')).toBe(false);
161
+ });
162
+ it('should handle caret ranges', () => {
163
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '^1.2.3')).toBe(true);
164
+ expect((0, repoDependencyFacts_1.semverValid)('1.3.0', '^1.2.3')).toBe(true);
165
+ expect((0, repoDependencyFacts_1.semverValid)('2.0.0', '^1.2.3')).toBe(false);
166
+ });
167
+ it('should handle tilde ranges', () => {
168
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '~1.2.3')).toBe(true);
169
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.9', '~1.2.3')).toBe(true);
170
+ expect((0, repoDependencyFacts_1.semverValid)('1.3.0', '~1.2.3')).toBe(false);
171
+ });
172
+ it('should handle x-ranges', () => {
173
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '1.2.x')).toBe(true);
174
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.9', '1.2.x')).toBe(true);
175
+ expect((0, repoDependencyFacts_1.semverValid)('1.3.0', '1.2.x')).toBe(false);
176
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '1.x')).toBe(true);
177
+ expect((0, repoDependencyFacts_1.semverValid)('1.3.0', '1.x')).toBe(true);
178
+ expect((0, repoDependencyFacts_1.semverValid)('2.0.0', '1.x')).toBe(false);
179
+ });
180
+ it('should handle star ranges', () => {
181
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '*')).toBe(true);
182
+ expect((0, repoDependencyFacts_1.semverValid)('2.0.0', '*')).toBe(true);
183
+ });
184
+ it('should handle greater than and less than ranges', () => {
185
+ expect((0, repoDependencyFacts_1.semverValid)('2.0.0', '>1.2.3')).toBe(true);
186
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '>1.2.3')).toBe(false);
187
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.2', '<1.2.3')).toBe(true);
188
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '<1.2.3')).toBe(false);
189
+ });
190
+ it('should handle AND ranges', () => {
191
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '>1.2.2 <1.2.4')).toBe(true);
192
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.4', '>1.2.2 <1.2.4')).toBe(false);
193
+ });
194
+ it('should handle OR ranges', () => {
195
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3', '1.2.3 || 1.2.4')).toBe(true);
196
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.4', '1.2.3 || 1.2.4')).toBe(true);
197
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.5', '1.2.3 || 1.2.4')).toBe(false);
198
+ });
199
+ it('should handle pre-release versions', () => {
200
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3-alpha', '>=1.2.3-alpha')).toBe(true);
201
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.3-beta', '>=1.2.3-alpha')).toBe(true);
202
+ expect((0, repoDependencyFacts_1.semverValid)('1.2.2', '>=1.2.3-alpha')).toBe(false);
160
203
  });
161
204
  it('should return true for empty strings', () => {
162
- expect(repoDependencyFacts.semverValid('', '')).toBe(true);
205
+ expect((0, repoDependencyFacts_1.semverValid)('', '')).toBe(true);
163
206
  });
164
207
  it('should return false for invalid input', () => {
165
- expect(repoDependencyFacts.semverValid('not-a-version', '1.0.0')).toBe(false);
166
- expect(repoDependencyFacts.semverValid('1.0.0', 'not-a-range')).toBe(false);
208
+ expect((0, repoDependencyFacts_1.semverValid)('not-a-version', '1.0.0')).toBe(false);
209
+ expect((0, repoDependencyFacts_1.semverValid)('1.0.0', 'not-a-range')).toBe(false);
167
210
  });
168
211
  });
169
212
  });
@@ -39,13 +39,13 @@ function collectRepoFileData(repoPath, archetypeConfig) {
39
39
  logger_1.logger.debug(`checking file: ${filePath}`);
40
40
  const stats = yield fs_1.default.promises.lstat(filePath);
41
41
  if (stats.isDirectory()) {
42
- if (!isBlacklisted(filePath, archetypeConfig.config.blacklistPatterns)) {
42
+ if (!isBlacklisted({ filePath, repoPath, blacklistPatterns: archetypeConfig.config.blacklistPatterns })) {
43
43
  const dirFilesData = yield collectRepoFileData(filePath, archetypeConfig);
44
44
  filesData.push(...dirFilesData);
45
45
  }
46
46
  }
47
47
  else {
48
- if (!isBlacklisted(filePath, archetypeConfig.config.blacklistPatterns) && isWhitelisted(filePath, archetypeConfig.config.whitelistPatterns)) {
48
+ if (!isBlacklisted({ filePath, repoPath, blacklistPatterns: archetypeConfig.config.blacklistPatterns }) && isWhitelisted({ filePath, repoPath, whitelistPatterns: archetypeConfig.config.whitelistPatterns })) {
49
49
  logger_1.logger.debug(`adding file: ${filePath}`);
50
50
  const fileData = yield parseFile(filePath);
51
51
  filesData.push(fileData);
@@ -55,21 +55,33 @@ function collectRepoFileData(repoPath, archetypeConfig) {
55
55
  return filesData;
56
56
  });
57
57
  }
58
- function isBlacklisted(filePath, blacklistPatterns) {
58
+ function isBlacklisted({ filePath, repoPath, blacklistPatterns }) {
59
59
  logger_1.logger.debug(`checking blacklist for file: ${filePath}`);
60
+ const normalizedPath = path_1.default.normalize(filePath);
61
+ const normalizedRepoPath = path_1.default.normalize(repoPath);
62
+ if (!normalizedPath.startsWith(normalizedRepoPath)) {
63
+ logger_1.logger.warn(`Potential path traversal attempt detected: ${filePath}`);
64
+ return true;
65
+ }
60
66
  for (const pattern of blacklistPatterns) {
61
- if (new RegExp(pattern).test(filePath)) {
62
- logger_1.logger.debug(`skipping blacklisted file: ${filePath} with pattern: ${pattern}`);
67
+ if (new RegExp(pattern).test(normalizedPath)) {
68
+ logger_1.logger.debug(`skipping blacklisted file: ${normalizedPath} with pattern: ${pattern}`);
63
69
  return true;
64
70
  }
65
71
  }
66
72
  return false;
67
73
  }
68
- function isWhitelisted(filePath, whitelistPatterns) {
74
+ function isWhitelisted({ filePath, repoPath, whitelistPatterns }) {
69
75
  logger_1.logger.debug(`checking whitelist for file: ${filePath}`);
76
+ const normalizedPath = path_1.default.normalize(filePath);
77
+ const normalizedRepoPath = path_1.default.normalize(repoPath);
78
+ if (!normalizedPath.startsWith(normalizedRepoPath)) {
79
+ logger_1.logger.warn(`Potential path traversal attempt detected: ${filePath}`);
80
+ return false;
81
+ }
70
82
  for (const pattern of whitelistPatterns) {
71
- if (new RegExp(pattern).test(filePath)) {
72
- logger_1.logger.debug(`allowing file: ${filePath} with pattern: ${pattern}`);
83
+ if (new RegExp(pattern).test(normalizedPath)) {
84
+ logger_1.logger.debug(`allowing file: ${normalizedPath} with pattern: ${pattern}`);
73
85
  return true;
74
86
  }
75
87
  }
@@ -60,22 +60,26 @@ describe('File operations', () => {
60
60
  });
61
61
  describe('isBlacklisted', () => {
62
62
  it('should return true if file path matches any blacklist pattern', () => {
63
- const filePath = '/node_modules/index.js';
64
- expect((0, repoFilesystemFacts_1.isBlacklisted)(filePath, mockArchetypeConfig.config.blacklistPatterns)).toBe(true);
63
+ const filePath = '/test/repo/node_modules/index.js';
64
+ const repoPath = '/test/repo';
65
+ expect((0, repoFilesystemFacts_1.isBlacklisted)({ filePath, repoPath, blacklistPatterns: mockArchetypeConfig.config.blacklistPatterns })).toBe(true);
65
66
  });
66
67
  it('should return false if file path does not match any blacklist pattern', () => {
67
- const filePath = 'src/index.js';
68
- expect((0, repoFilesystemFacts_1.isBlacklisted)(filePath, mockArchetypeConfig.config.blacklistPatterns)).toBe(false);
68
+ const filePath = '/test/repo/src/index.js';
69
+ const repoPath = '/test/repo';
70
+ expect((0, repoFilesystemFacts_1.isBlacklisted)({ filePath, repoPath, blacklistPatterns: mockArchetypeConfig.config.blacklistPatterns })).toBe(false);
69
71
  });
70
72
  });
71
73
  describe('isWhitelisted', () => {
72
74
  it('should return true if file path matches any whitelist pattern', () => {
73
- const filePath = '/src/index.js';
74
- expect((0, repoFilesystemFacts_1.isWhitelisted)(filePath, mockArchetypeConfig.config.whitelistPatterns)).toBe(true);
75
+ const filePath = '/test/repo/src/index.js';
76
+ const repoPath = '/test/repo';
77
+ expect((0, repoFilesystemFacts_1.isWhitelisted)({ filePath, repoPath, whitelistPatterns: mockArchetypeConfig.config.whitelistPatterns })).toBe(true);
75
78
  });
76
79
  it('should return false if file path does not match any whitelist pattern', () => {
77
- const filePath = 'build/index.txt';
78
- expect((0, repoFilesystemFacts_1.isWhitelisted)(filePath, mockArchetypeConfig.config.whitelistPatterns)).toBe(false);
80
+ const filePath = '/test/repo/build/index.txt';
81
+ const repoPath = '/test/repo';
82
+ expect((0, repoFilesystemFacts_1.isWhitelisted)({ filePath, repoPath, whitelistPatterns: mockArchetypeConfig.config.whitelistPatterns })).toBe(false);
79
83
  });
80
84
  });
81
85
  });
@@ -8,7 +8,7 @@ const outdatedFramework = {
8
8
  var _a;
9
9
  let result = false;
10
10
  try {
11
- logger_1.logger.debug(`outdatedFramework: processing ${repoDependencyAnalysis}`);
11
+ logger_1.logger.debug(`outdatedFramework: processing ${JSON.stringify(repoDependencyAnalysis)}`);
12
12
  if (((_a = repoDependencyAnalysis === null || repoDependencyAnalysis === void 0 ? void 0 : repoDependencyAnalysis.result) === null || _a === void 0 ? void 0 : _a.length) > 0) {
13
13
  return true;
14
14
  }
@@ -27,7 +27,7 @@ const validateGithubWebhook_1 = require("./middleware/validateGithubWebhook");
27
27
  const chokidar_1 = __importDefault(require("chokidar"));
28
28
  const cacheManager_1 = require("./cacheManager");
29
29
  const SHARED_SECRET = process.env.XFI_SHARED_SECRET;
30
- const maskedSecret = SHARED_SECRET ? `${SHARED_SECRET.substring(0, 4)}****${SHARED_SECRET.substring(SHARED_SECRET.length - 4)}` : 'not set';
30
+ const maskedSecret = SHARED_SECRET ? `${SHARED_SECRET.substring(0, 1)}****${SHARED_SECRET.substring(SHARED_SECRET.length - 1)}` : 'not set';
31
31
  logger_1.logger.info(`Shared secret is ${maskedSecret}`);
32
32
  const app = (0, express_1.default)();
33
33
  // Add security headers
@@ -3,10 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.validateRule = exports.validateArchetype = void 0;
6
+ exports.validateRule = exports.validateArchetype = exports.validateRuleSchema = exports.validateArchetypeSchema = void 0;
7
7
  const ajv_1 = __importDefault(require("ajv"));
8
8
  const logger_1 = require("./logger");
9
+ const semver_1 = __importDefault(require("semver"));
9
10
  const ajv = new ajv_1.default();
11
+ ajv.addFormat("semverPattern", {
12
+ type: "string",
13
+ validate: (x) => semver_1.default.valid(x) !== null && semver_1.default.validRange(x) !== null
14
+ });
10
15
  const archetypeSchema = {
11
16
  type: 'object',
12
17
  properties: {
@@ -19,13 +24,19 @@ const archetypeSchema = {
19
24
  properties: {
20
25
  minimumDependencyVersions: {
21
26
  type: 'object',
27
+ properties: {
28
+ '^.*$': {
29
+ type: 'string',
30
+ format: 'semverPattern'
31
+ }
32
+ },
22
33
  minProperties: 1,
34
+ additionalProperties: true,
23
35
  required: []
24
36
  },
25
37
  standardStructure: {
26
38
  type: 'object',
27
39
  minProperties: 1,
28
- required: []
29
40
  },
30
41
  blacklistPatterns: {
31
42
  type: 'array',
@@ -42,7 +53,7 @@ const archetypeSchema = {
42
53
  additionalProperties: true
43
54
  }
44
55
  },
45
- required: ['rules', 'operators', 'facts', 'config'],
56
+ required: ['name', 'rules', 'operators', 'facts', 'config'],
46
57
  additionalProperties: false
47
58
  };
48
59
  const ruleSchema = {
@@ -71,8 +82,8 @@ const ruleSchema = {
71
82
  },
72
83
  required: ['name', 'conditions', 'event']
73
84
  };
74
- const validateArchetypeSchema = ajv.compile(archetypeSchema);
75
- const validateRuleSchema = ajv.compile(ruleSchema);
85
+ exports.validateArchetypeSchema = ajv.compile(archetypeSchema);
86
+ exports.validateRuleSchema = ajv.compile(ruleSchema);
76
87
  // Helper function to log validation errors
77
88
  const logValidationErrors = (errors) => {
78
89
  if (errors) {
@@ -83,17 +94,17 @@ const logValidationErrors = (errors) => {
83
94
  };
84
95
  // Wrap the validate functions to log errors
85
96
  const validateArchetype = (data) => {
86
- const isValid = validateArchetypeSchema(data);
97
+ const isValid = (0, exports.validateArchetypeSchema)(data);
87
98
  if (!isValid) {
88
- logValidationErrors(validateArchetypeSchema.errors);
99
+ logValidationErrors(exports.validateArchetypeSchema.errors);
89
100
  }
90
101
  return isValid;
91
102
  };
92
103
  exports.validateArchetype = validateArchetype;
93
104
  const validateRule = (data) => {
94
- const isValid = validateRuleSchema(data);
105
+ const isValid = (0, exports.validateRuleSchema)(data);
95
106
  if (!isValid) {
96
- logValidationErrors(validateRuleSchema.errors);
107
+ logValidationErrors(exports.validateRuleSchema.errors);
97
108
  }
98
109
  return isValid;
99
110
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x-fidelity",
3
- "version": "2.12.1",
3
+ "version": "2.13.1",
4
4
  "description": "cli for opinionated framework adherence checks",
5
5
  "main": "dist/xfidelity",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "config": {
19
19
  "minimumDependencyVersions": {
20
- "spring-boot-starter": "^2.5.0",
20
+ "springbootstarter": ">2.5.0",
21
21
  "spring-boot-starter-web": "^2.5.0"
22
22
  },
23
23
  "standardStructure": {
@@ -21,11 +21,12 @@
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",
27
- "commander": "^2.0.0",
28
- "nodemon": "^3.9.0"
24
+ "@types/react": ">=18.0.0",
25
+ "react": "~17.0.0",
26
+ "@yarnpkg/lockfile": "<1.2.0",
27
+ "commander": ">=2.0.0 <13.0.0",
28
+ "nodemon": ">4.9.0",
29
+ "@colors/colors": "1.7.0 || 1.6.0"
29
30
  },
30
31
  "standardStructure": {
31
32
  "app": {
@@ -83,35 +83,6 @@ describe('setupEngine', () => {
83
83
  expect(engine).toBeDefined();
84
84
  });
85
85
 
86
- it('should handle errors when loading rules and add default rules', async () => {
87
- const mockAddRule = jest.fn();
88
- (Engine as jest.Mock).mockImplementation(() => ({
89
- addOperator: jest.fn(),
90
- addRule: mockAddRule,
91
- addFact: jest.fn(),
92
- on: jest.fn()
93
- }));
94
-
95
- (ConfigManager.getConfig as jest.Mock).mockResolvedValue({
96
- archetype: mockArchetypeConfig,
97
- rules: [
98
- undefined,
99
- { name: undefined },
100
- { name: 'invalidRule', conditions: null }
101
- ],
102
- cliOptions: {}
103
- });
104
-
105
- await setupEngine(mockParams);
106
-
107
- expect(logger.error).toHaveBeenCalledWith('Invalid rule configuration: rule or rule name is undefined');
108
- expect(logger.error).toHaveBeenCalledWith('Invalid rule configuration: rule or rule name is undefined');
109
- expect(logger.error).toHaveBeenCalledWith('Error loading rule: invalidRule');
110
- expect(logger.info).toHaveBeenCalledWith('No valid rules were added. Adding default rules.');
111
- expect(mockAddRule).toHaveBeenCalledWith(expect.objectContaining({ name: 'default-rule-1' }));
112
- expect(mockAddRule).toHaveBeenCalledWith(expect.objectContaining({ name: 'default-rule-2' }));
113
- });
114
-
115
86
  it('should set up event listeners for success events', async () => {
116
87
  const mockOn = jest.fn();
117
88
  (Engine as jest.Mock).mockImplementation(() => ({
@@ -25,7 +25,7 @@ export async function setupEngine(params: SetupEngineParams): Promise<Engine> {
25
25
  logger.info(`=== loading json rules..`);
26
26
  const config = await ConfigManager.getConfig({ archetype, logPrefix: executionLogPrefix });
27
27
 
28
- logger.debug(config.rules);
28
+ logger.debug(`rules loaded: ${config.rules}`);
29
29
 
30
30
  const addedRules = new Set();
31
31
  config.rules.forEach((rule) => {
@@ -51,20 +51,6 @@ export async function setupEngine(params: SetupEngineParams): Promise<Engine> {
51
51
  }
52
52
  });
53
53
 
54
- if (addedRules.size === 0) {
55
- logger.info('No valid rules were added. Adding default rules.');
56
- engine.addRule({
57
- name: 'default-rule-1',
58
- conditions: { all: [] },
59
- event: { type: 'warning', params: { message: 'Default rule 1 triggered' } }
60
- });
61
- engine.addRule({
62
- name: 'default-rule-2',
63
- conditions: { all: [] },
64
- event: { type: 'warning', params: { message: 'Default rule 2 triggered' } }
65
- });
66
- }
67
-
68
54
  engine.on('success', async ({ type, params }: Event) => {
69
55
  if (type === 'warning') {
70
56
  logger.warn(`warning detected: ${JSON.stringify(params)}`);
@@ -3,6 +3,7 @@ import { execSync } from 'child_process';
3
3
  import fs from 'fs';
4
4
  import { Almanac } from 'json-rules-engine';
5
5
  import { LocalDependencies, MinimumDepVersions } from '../types/typeDefs';
6
+ import { semverValid } from './repoDependencyFacts';
6
7
 
7
8
  jest.mock('child_process');
8
9
  jest.mock('fs');
@@ -131,32 +132,82 @@ describe('repoDependencyFacts', () => {
131
132
 
132
133
  describe('semverValid', () => {
133
134
  it('should return true for valid version comparisons', () => {
134
- expect(repoDependencyFacts.semverValid('2.0.0', '^1.0.0')).toBe(false);
135
- expect(repoDependencyFacts.semverValid('1.5.0', '1.0.0 - 2.0.0')).toBe(true);
136
- expect(repoDependencyFacts.semverValid('1.0.0', '1.0.0')).toBe(true);
137
- expect(repoDependencyFacts.semverValid('2.0.0', '>=1.0.0')).toBe(true);
135
+ expect(semverValid('2.0.0', '^1.0.0')).toBe(false);
136
+ expect(semverValid('1.5.0', '1.0.0 - 2.0.0')).toBe(true);
137
+ expect(semverValid('1.0.0', '1.0.0')).toBe(true);
138
+ expect(semverValid('2.0.0', '>=1.0.0')).toBe(true);
138
139
  });
139
-
140
+
140
141
  it('should return false for invalid version comparisons', () => {
141
- expect(repoDependencyFacts.semverValid('1.0.0', '^2.0.0')).toBe(false);
142
- expect(repoDependencyFacts.semverValid('3.0.0', '1.0.0 - 2.0.0')).toBe(false);
143
- expect(repoDependencyFacts.semverValid('0.9.0', '>=1.0.0')).toBe(false);
142
+ expect(semverValid('1.0.0', '^2.0.0')).toBe(false);
143
+ expect(semverValid('3.0.0', '1.0.0 - 2.0.0')).toBe(false);
144
+ expect(semverValid('0.9.0', '>=1.0.0')).toBe(false);
144
145
  });
145
-
146
+
146
147
  it('should handle complex version ranges', () => {
147
- expect(repoDependencyFacts.semverValid('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
148
- expect(repoDependencyFacts.semverValid('2.5.0', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
149
- expect(repoDependencyFacts.semverValid('5.5.5', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
150
- expect(repoDependencyFacts.semverValid('8.0.0', '1.x || >=9.5.0 || 5.0.0 - 7.2.3')).toBe(false);
148
+ expect(semverValid('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
149
+ expect(semverValid('2.5.0', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
150
+ expect(semverValid('5.5.5', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')).toBe(true);
151
+ expect(semverValid('8.0.0', '1.x || >=9.5.0 || 5.0.0 - 7.2.3')).toBe(false);
151
152
  });
152
-
153
+
154
+ it('should handle caret ranges', () => {
155
+ expect(semverValid('1.2.3', '^1.2.3')).toBe(true);
156
+ expect(semverValid('1.3.0', '^1.2.3')).toBe(true);
157
+ expect(semverValid('2.0.0', '^1.2.3')).toBe(false);
158
+ });
159
+
160
+ it('should handle tilde ranges', () => {
161
+ expect(semverValid('1.2.3', '~1.2.3')).toBe(true);
162
+ expect(semverValid('1.2.9', '~1.2.3')).toBe(true);
163
+ expect(semverValid('1.3.0', '~1.2.3')).toBe(false);
164
+ });
165
+
166
+ it('should handle x-ranges', () => {
167
+ expect(semverValid('1.2.3', '1.2.x')).toBe(true);
168
+ expect(semverValid('1.2.9', '1.2.x')).toBe(true);
169
+ expect(semverValid('1.3.0', '1.2.x')).toBe(false);
170
+ expect(semverValid('1.2.3', '1.x')).toBe(true);
171
+ expect(semverValid('1.3.0', '1.x')).toBe(true);
172
+ expect(semverValid('2.0.0', '1.x')).toBe(false);
173
+ });
174
+
175
+ it('should handle star ranges', () => {
176
+ expect(semverValid('1.2.3', '*')).toBe(true);
177
+ expect(semverValid('2.0.0', '*')).toBe(true);
178
+ });
179
+
180
+ it('should handle greater than and less than ranges', () => {
181
+ expect(semverValid('2.0.0', '>1.2.3')).toBe(true);
182
+ expect(semverValid('1.2.3', '>1.2.3')).toBe(false);
183
+ expect(semverValid('1.2.2', '<1.2.3')).toBe(true);
184
+ expect(semverValid('1.2.3', '<1.2.3')).toBe(false);
185
+ });
186
+
187
+ it('should handle AND ranges', () => {
188
+ expect(semverValid('1.2.3', '>1.2.2 <1.2.4')).toBe(true);
189
+ expect(semverValid('1.2.4', '>1.2.2 <1.2.4')).toBe(false);
190
+ });
191
+
192
+ it('should handle OR ranges', () => {
193
+ expect(semverValid('1.2.3', '1.2.3 || 1.2.4')).toBe(true);
194
+ expect(semverValid('1.2.4', '1.2.3 || 1.2.4')).toBe(true);
195
+ expect(semverValid('1.2.5', '1.2.3 || 1.2.4')).toBe(false);
196
+ });
197
+
198
+ it('should handle pre-release versions', () => {
199
+ expect(semverValid('1.2.3-alpha', '>=1.2.3-alpha')).toBe(true);
200
+ expect(semverValid('1.2.3-beta', '>=1.2.3-alpha')).toBe(true);
201
+ expect(semverValid('1.2.2', '>=1.2.3-alpha')).toBe(false);
202
+ });
203
+
153
204
  it('should return true for empty strings', () => {
154
- expect(repoDependencyFacts.semverValid('', '')).toBe(true);
205
+ expect(semverValid('', '')).toBe(true);
155
206
  });
156
-
207
+
157
208
  it('should return false for invalid input', () => {
158
- expect(repoDependencyFacts.semverValid('not-a-version', '1.0.0')).toBe(false);
159
- expect(repoDependencyFacts.semverValid('1.0.0', 'not-a-range')).toBe(false);
209
+ expect(semverValid('not-a-version', '1.0.0')).toBe(false);
210
+ expect(semverValid('1.0.0', 'not-a-range')).toBe(false);
160
211
  });
161
212
  });
162
213
  });
@@ -186,35 +186,46 @@ export async function repoDependencyAnalysis(params: any, almanac: Almanac) {
186
186
  }
187
187
 
188
188
  export function semverValid(installed: string, required: string): boolean {
189
+
189
190
  // Remove potential @namespace from installed version
190
191
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191
- const installedVersion = installed.includes('@') ? installed.split('@').pop()! : installed;
192
-
193
- if (!required || !installedVersion) {
192
+ installed = installed.includes('@') ? installed.split('@').pop()! : installed;
193
+
194
+ if (!installed || !required) {
194
195
  return true;
195
196
  }
196
197
 
198
+ // ensure that both inputs are now valid semver or range strings
199
+ if (!semver.valid(installed) && !semver.validRange(installed)) {
200
+ logger.error(`semverValid: invalid installed version or range: ${installed}`);
201
+ return false;
202
+ }
203
+ if (!semver.valid(required) && !semver.validRange(required)) {
204
+ logger.error(`semverValid: invalid required version or range: ${required}`);
205
+ return false;
206
+ }
207
+
197
208
  // If 'installed' is a single version and 'required' is a range
198
209
  if (semver.valid(installed) && semver.validRange(required)) {
199
- logger.debug('range vs version');
210
+ logger.info('range vs version');
200
211
  return semver.satisfies(installed, required);
201
212
  }
202
213
 
203
214
  // If 'required' is a single version and 'installed' is a range
204
215
  if (semver.valid(required) && semver.validRange(installed)) {
205
- logger.debug('version vs range');
216
+ logger.info('version vs range');
206
217
  return semver.satisfies(required, installed);
207
218
  }
208
219
 
209
220
  // If both are single versions, simply compare them
210
221
  if (semver.valid(required) && semver.valid(installed)) {
211
- logger.debug('version vs version');
222
+ logger.info('version vs version');
212
223
  return semver.gt(installed, required);
213
224
  }
214
225
 
215
226
  // If both are ranges, check if they intersect
216
227
  if (semver.validRange(required) && semver.validRange(installed)) {
217
- logger.debug('range vs range');
228
+ logger.info('range vs range');
218
229
  return semver.intersects(required, installed);
219
230
  }
220
231
 
@@ -60,25 +60,29 @@ describe('File operations', () => {
60
60
 
61
61
  describe('isBlacklisted', () => {
62
62
  it('should return true if file path matches any blacklist pattern', () => {
63
- const filePath = '/node_modules/index.js';
64
- expect(isBlacklisted(filePath, mockArchetypeConfig.config.blacklistPatterns)).toBe(true);
63
+ const filePath = '/test/repo/node_modules/index.js';
64
+ const repoPath = '/test/repo';
65
+ expect(isBlacklisted({ filePath, repoPath, blacklistPatterns: mockArchetypeConfig.config.blacklistPatterns })).toBe(true);
65
66
  });
66
67
 
67
68
  it('should return false if file path does not match any blacklist pattern', () => {
68
- const filePath = 'src/index.js';
69
- expect(isBlacklisted(filePath, mockArchetypeConfig.config.blacklistPatterns)).toBe(false);
69
+ const filePath = '/test/repo/src/index.js';
70
+ const repoPath = '/test/repo';
71
+ expect(isBlacklisted({ filePath, repoPath, blacklistPatterns: mockArchetypeConfig.config.blacklistPatterns })).toBe(false);
70
72
  });
71
73
  });
72
74
 
73
75
  describe('isWhitelisted', () => {
74
76
  it('should return true if file path matches any whitelist pattern', () => {
75
- const filePath = '/src/index.js';
76
- expect(isWhitelisted(filePath, mockArchetypeConfig.config.whitelistPatterns)).toBe(true);
77
+ const filePath = '/test/repo/src/index.js';
78
+ const repoPath = '/test/repo';
79
+ expect(isWhitelisted({ filePath, repoPath, whitelistPatterns: mockArchetypeConfig.config.whitelistPatterns })).toBe(true);
77
80
  });
78
81
 
79
82
  it('should return false if file path does not match any whitelist pattern', () => {
80
- const filePath = 'build/index.txt';
81
- expect(isWhitelisted(filePath, mockArchetypeConfig.config.whitelistPatterns)).toBe(false);
83
+ const filePath = '/test/repo/build/index.txt';
84
+ const repoPath = '/test/repo';
85
+ expect(isWhitelisted({ filePath, repoPath, whitelistPatterns: mockArchetypeConfig.config.whitelistPatterns })).toBe(false);
82
86
  });
83
87
  });
84
88
  });
@@ -1,7 +1,7 @@
1
1
  import { logger } from '../utils/logger';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { ArchetypeConfig } from '../types/typeDefs';
4
+ import { ArchetypeConfig, IsBlacklistedParams, isWhitelistedParams } from '../types/typeDefs';
5
5
 
6
6
  interface FileData {
7
7
  fileName: string;
@@ -29,12 +29,12 @@ async function collectRepoFileData(repoPath: string, archetypeConfig: ArchetypeC
29
29
  logger.debug(`checking file: ${filePath}`);
30
30
  const stats = await fs.promises.lstat(filePath);
31
31
  if (stats.isDirectory()) {
32
- if (!isBlacklisted(filePath, archetypeConfig.config.blacklistPatterns)) {
32
+ if (!isBlacklisted({ filePath, repoPath, blacklistPatterns: archetypeConfig.config.blacklistPatterns })) {
33
33
  const dirFilesData = await collectRepoFileData(filePath, archetypeConfig);
34
34
  filesData.push(...dirFilesData);
35
35
  }
36
36
  } else {
37
- if (!isBlacklisted(filePath, archetypeConfig.config.blacklistPatterns) && isWhitelisted(filePath, archetypeConfig.config.whitelistPatterns)) {
37
+ if (!isBlacklisted({ filePath, repoPath, blacklistPatterns: archetypeConfig.config.blacklistPatterns }) && isWhitelisted({ filePath, repoPath, whitelistPatterns: archetypeConfig.config.whitelistPatterns })) {
38
38
  logger.debug(`adding file: ${filePath}`);
39
39
  const fileData = await parseFile(filePath);
40
40
  filesData.push(fileData);
@@ -45,22 +45,38 @@ async function collectRepoFileData(repoPath: string, archetypeConfig: ArchetypeC
45
45
  return filesData;
46
46
  }
47
47
 
48
- function isBlacklisted(filePath: string, blacklistPatterns: string[]): boolean {
48
+ function isBlacklisted({ filePath, repoPath, blacklistPatterns }: IsBlacklistedParams): boolean {
49
49
  logger.debug(`checking blacklist for file: ${filePath}`);
50
+ const normalizedPath = path.normalize(filePath);
51
+ const normalizedRepoPath = path.normalize(repoPath);
52
+
53
+ if (!normalizedPath.startsWith(normalizedRepoPath)) {
54
+ logger.warn(`Potential path traversal attempt detected: ${filePath}`);
55
+ return true;
56
+ }
57
+
50
58
  for (const pattern of blacklistPatterns) {
51
- if (new RegExp(pattern).test(filePath)) {
52
- logger.debug(`skipping blacklisted file: ${filePath} with pattern: ${pattern}`);
59
+ if (new RegExp(pattern).test(normalizedPath)) {
60
+ logger.debug(`skipping blacklisted file: ${normalizedPath} with pattern: ${pattern}`);
53
61
  return true;
54
62
  }
55
63
  }
56
64
  return false;
57
65
  }
58
66
 
59
- function isWhitelisted(filePath: string, whitelistPatterns: string[]): boolean {
67
+ function isWhitelisted({ filePath, repoPath, whitelistPatterns }: isWhitelistedParams): boolean {
60
68
  logger.debug(`checking whitelist for file: ${filePath}`);
69
+ const normalizedPath = path.normalize(filePath);
70
+ const normalizedRepoPath = path.normalize(repoPath);
71
+
72
+ if (!normalizedPath.startsWith(normalizedRepoPath)) {
73
+ logger.warn(`Potential path traversal attempt detected: ${filePath}`);
74
+ return false;
75
+ }
76
+
61
77
  for (const pattern of whitelistPatterns) {
62
- if (new RegExp(pattern).test(filePath)) {
63
- logger.debug(`allowing file: ${filePath} with pattern: ${pattern}`);
78
+ if (new RegExp(pattern).test(normalizedPath)) {
79
+ logger.debug(`allowing file: ${normalizedPath} with pattern: ${pattern}`);
64
80
  return true;
65
81
  }
66
82
  }
@@ -7,7 +7,7 @@ const outdatedFramework: OperatorDefn = {
7
7
  let result = false;
8
8
 
9
9
  try {
10
- logger.debug(`outdatedFramework: processing ${repoDependencyAnalysis}`);
10
+ logger.debug(`outdatedFramework: processing ${JSON.stringify(repoDependencyAnalysis)}`);
11
11
 
12
12
  if (repoDependencyAnalysis?.result?.length > 0) {
13
13
  return true;
@@ -23,7 +23,7 @@ import chokidar from 'chokidar';
23
23
  import { handleConfigChange } from './cacheManager';
24
24
 
25
25
  const SHARED_SECRET = process.env.XFI_SHARED_SECRET;
26
- const maskedSecret = SHARED_SECRET ? `${SHARED_SECRET.substring(0, 4)}****${SHARED_SECRET.substring(SHARED_SECRET.length - 4)}` : 'not set';
26
+ const maskedSecret = SHARED_SECRET ? `${SHARED_SECRET.substring(0, 1)}****${SHARED_SECRET.substring(SHARED_SECRET.length - 1)}` : 'not set';
27
27
  logger.info(`Shared secret is ${maskedSecret}`);
28
28
 
29
29
  const app = express();
@@ -1,5 +1,6 @@
1
1
  import { Engine, OperatorEvaluator } from 'json-rules-engine';
2
2
  import { FileData } from '../facts/repoFilesystemFacts';
3
+ import { JSONSchemaType } from 'ajv';
3
4
 
4
5
  export type OperatorDefn = {
5
6
  name: string,
@@ -89,13 +90,16 @@ export interface ArchetypeConfig {
89
90
  operators: string[];
90
91
  facts: string[];
91
92
  config: {
92
- minimumDependencyVersions: Record<string, string>;
93
+ minimumDependencyVersions: Record<string, string>; // This will be validated in the JSON schema
93
94
  standardStructure: Record<string, any>;
94
95
  blacklistPatterns: string[];
95
96
  whitelistPatterns: string[];
96
97
  };
97
98
  }
98
99
 
100
+ // Add a new type for the JSON schema
101
+ export type ArchetypeConfigSchema = JSONSchemaType<ArchetypeConfig>;
102
+
99
103
  export interface RuleConfig {
100
104
  name: string;
101
105
  conditions: {
@@ -107,6 +111,7 @@ export interface RuleConfig {
107
111
  params: Record<string, any>;
108
112
  };
109
113
  }
114
+ export type RuleConfigSchema = JSONSchemaType<RuleConfig>;
110
115
 
111
116
  export interface OpenAIAnalysisParams {
112
117
  prompt: string;
@@ -217,3 +222,14 @@ export interface LoadExemptionsParams {
217
222
  archetype: string;
218
223
  }
219
224
 
225
+ export interface IsBlacklistedParams {
226
+ filePath: string;
227
+ repoPath: string;
228
+ blacklistPatterns: string[];
229
+ }
230
+
231
+ export interface isWhitelistedParams {
232
+ filePath: string;
233
+ repoPath: string;
234
+ whitelistPatterns: string[];
235
+ }
@@ -1,10 +1,16 @@
1
- import Ajv, { JSONSchemaType } from 'ajv';
1
+ import Ajv from 'ajv';
2
2
  import { logger } from './logger';
3
- import { ArchetypeConfig, RuleConfig } from '../types/typeDefs';
3
+ import { ArchetypeConfigSchema, RuleConfigSchema } from '../types/typeDefs';
4
+ import semver from 'semver';
4
5
 
5
6
  const ajv = new Ajv();
6
7
 
7
- const archetypeSchema: JSONSchemaType<ArchetypeConfig> = {
8
+ ajv.addFormat("semverPattern", {
9
+ type: "string",
10
+ validate: (x) => semver.valid(x) !== null && semver.validRange(x) !== null
11
+ })
12
+
13
+ const archetypeSchema: ArchetypeConfigSchema = {
8
14
  type: 'object',
9
15
  properties: {
10
16
  name: { type: 'string' },
@@ -16,34 +22,40 @@ const archetypeSchema: JSONSchemaType<ArchetypeConfig> = {
16
22
  properties: {
17
23
  minimumDependencyVersions: {
18
24
  type: 'object',
25
+ properties: {
26
+ '^.*$': {
27
+ type: 'string',
28
+ format: 'semverPattern'
29
+ }
30
+ },
19
31
  minProperties: 1,
32
+ additionalProperties: true,
20
33
  required: []
21
34
  },
22
35
  standardStructure: {
23
36
  type: 'object',
24
37
  minProperties: 1,
25
- required: []
26
38
  },
27
39
  blacklistPatterns: {
28
40
  type: 'array',
29
- items: { type: 'string'},
41
+ items: { type: 'string' },
30
42
  minItems: 1
31
43
  },
32
44
  whitelistPatterns: {
33
45
  type: 'array',
34
- items: { type: 'string'},
46
+ items: { type: 'string' },
35
47
  minItems: 1
36
48
  }
37
49
  },
38
- required: ['minimumDependencyVersions', 'standardStructure', 'blacklistPatterns', 'whitelistPatterns'],
50
+ required: ['minimumDependencyVersions', 'standardStructure', 'blacklistPatterns', 'whitelistPatterns'] as const,
39
51
  additionalProperties: true
40
52
  }
41
53
  },
42
- required: ['rules', 'operators', 'facts', 'config'],
54
+ required: ['name', 'rules', 'operators', 'facts', 'config'] as const,
43
55
  additionalProperties: false
44
- };
56
+ } as const;
45
57
 
46
- const ruleSchema: JSONSchemaType<RuleConfig> = {
58
+ const ruleSchema: RuleConfigSchema = {
47
59
  type: 'object',
48
60
  properties: {
49
61
  name: { type: 'string' },
@@ -70,8 +82,8 @@ const ruleSchema: JSONSchemaType<RuleConfig> = {
70
82
  required: ['name', 'conditions', 'event']
71
83
  };
72
84
 
73
- const validateArchetypeSchema = ajv.compile(archetypeSchema);
74
- const validateRuleSchema = ajv.compile(ruleSchema);
85
+ export const validateArchetypeSchema = ajv.compile(archetypeSchema);
86
+ export const validateRuleSchema = ajv.compile(ruleSchema);
75
87
 
76
88
  // Helper function to log validation errors
77
89
  const logValidationErrors = (errors: any[] | null | undefined) => {