x-fidelity 1.5.0 → 1.5.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 +7 -0
- package/README.md +18 -0
- package/dist/archetypes/index.js +1 -5
- package/dist/core/cli.js +6 -4
- package/dist/core/engine.js +44 -6
- package/dist/core/engine.test.js +2 -2
- package/dist/rules/index.js +26 -1
- package/dist/utils/config.js +62 -1
- package/dist/utils/telemetry.js +22 -3
- package/package.json +1 -1
- package/src/archetypes/index.ts +1 -5
- package/src/core/cli.ts +6 -4
- package/src/core/engine.test.ts +2 -2
- package/src/core/engine.ts +44 -6
- package/src/rules/index.ts +22 -1
- package/src/utils/config.ts +42 -1
- package/src/utils/telemetry.ts +21 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [1.5.1](https://github.com/zotoio/x-fidelity/compare/v1.5.0...v1.5.1) (2024-07-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **scope:** logging correlation ([ea1c497](https://github.com/zotoio/x-fidelity/commit/ea1c4977cac418c49cd9f6778e7f72458c2c2a61))
|
|
7
|
+
|
|
1
8
|
# [1.5.0](https://github.com/zotoio/x-fidelity/compare/v1.4.1...v1.5.0) (2024-07-24)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
x-fidelity is an advanced CLI tool designed to enforce opinionated framework adherence checks within a codebase. It provides a flexible and extensible way to ensure your projects follow specific standards and best practices.
|
|
4
4
|
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
1. Install x-fidelity:
|
|
8
|
+
```
|
|
9
|
+
yarn global add x-fidelity
|
|
10
|
+
export PATH="$PATH:$(yarn global bin)"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Run in your project directory:
|
|
14
|
+
```
|
|
15
|
+
xfidelity
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
3. For more options:
|
|
19
|
+
```
|
|
20
|
+
xfidelity --help
|
|
21
|
+
```
|
|
22
|
+
|
|
5
23
|
```
|
|
6
24
|
=====================================
|
|
7
25
|
__ __ ________ ______
|
package/dist/archetypes/index.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.archetypes = void 0;
|
|
4
4
|
exports.archetypes = {
|
|
5
5
|
'node-fullstack': {
|
|
6
|
-
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global'],
|
|
6
|
+
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global', 'yarnLockfileCheck-global'],
|
|
7
7
|
operators: ['fileContains', 'outdatedFramework', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'],
|
|
8
8
|
facts: ['repoFilesystemFacts', 'repoDependencyFacts', 'openaiAnalysisFacts'],
|
|
9
9
|
config: {
|
|
@@ -16,10 +16,6 @@ exports.archetypes = {
|
|
|
16
16
|
frontend: null,
|
|
17
17
|
server: null
|
|
18
18
|
}
|
|
19
|
-
// app: {
|
|
20
|
-
// frontend: 'required',
|
|
21
|
-
// server: 'required'
|
|
22
|
-
// }
|
|
23
19
|
},
|
|
24
20
|
blacklistPatterns: [
|
|
25
21
|
'.*\\/\\..*', // dot files
|
package/dist/core/cli.js
CHANGED
|
@@ -9,11 +9,12 @@ if (!logger_1.logger || typeof logger_1.logger.info !== 'function') {
|
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
11
|
commander_1.program
|
|
12
|
-
.option("-d, --dir <directory>", "The checkout directory to analyze", ".")
|
|
13
|
-
.option("-a, --archetype <archetype>", "The archetype to use for analysis", "node-fullstack")
|
|
12
|
+
.option("-d, --dir <directory>", "The checkout directory to analyze (default: current directory)", ".")
|
|
13
|
+
.option("-a, --archetype <archetype>", "The archetype to use for analysis (default: node-fullstack)", "node-fullstack")
|
|
14
14
|
.option("-c, --configServer <configServer>", "The config server URL for fetching remote archetype configurations and rules")
|
|
15
|
-
.option("-m, --mode <mode>", "Run mode: 'cli' or 'server'", "cli")
|
|
16
|
-
.option("-p, --port <port>", "Port number for server mode", "8888")
|
|
15
|
+
.option("-m, --mode <mode>", "Run mode: 'cli' or 'server' (default: cli)", "cli")
|
|
16
|
+
.option("-p, --port <port>", "Port number for server mode (default: 8888)", "8888")
|
|
17
|
+
.option("-l, --local-config <path>", "Path to local archetype config and rules");
|
|
17
18
|
commander_1.program.parse();
|
|
18
19
|
const options = commander_1.program.opts();
|
|
19
20
|
exports.options = options;
|
|
@@ -35,6 +36,7 @@ directory: ${process.env.PWD}/${options.dir}
|
|
|
35
36
|
configServer: ${options.configServer ? options.configServer : 'none'}
|
|
36
37
|
mode: ${options.mode}
|
|
37
38
|
port: ${options.mode === 'server' ? options.port : 'N/A'}
|
|
39
|
+
local-config: ${options.localConfig ? options.localConfig : 'none'}
|
|
38
40
|
for available options run: xfidelity --help
|
|
39
41
|
=====================================`);
|
|
40
42
|
logger_1.logger.info(banner);
|
package/dist/core/engine.js
CHANGED
|
@@ -8,6 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
exports.analyzeCodebase = analyzeCodebase;
|
|
13
16
|
const logger_1 = require("../utils/logger");
|
|
@@ -20,10 +23,12 @@ const facts_1 = require("../facts");
|
|
|
20
23
|
const rules_1 = require("../rules");
|
|
21
24
|
const config_1 = require("../utils/config");
|
|
22
25
|
const telemetry_1 = require("../utils/telemetry");
|
|
26
|
+
const child_process_1 = require("child_process");
|
|
27
|
+
const os_1 = __importDefault(require("os"));
|
|
23
28
|
function analyzeCodebase(repoPath_1) {
|
|
24
|
-
return __awaiter(this, arguments, void 0, function* (repoPath, archetype = 'node-fullstack', configServer = '') {
|
|
29
|
+
return __awaiter(this, arguments, void 0, function* (repoPath, archetype = 'node-fullstack', configServer = '', localConfigPath = '') {
|
|
25
30
|
const configManager = config_1.ConfigManager.getInstance();
|
|
26
|
-
yield configManager.initialize(archetype, configServer);
|
|
31
|
+
yield configManager.initialize(archetype, configServer, localConfigPath);
|
|
27
32
|
const archetypeConfig = configManager.getConfig();
|
|
28
33
|
const installedDependencyVersions = yield (0, repoDependencyFacts_1.getDependencyVersionFacts)(archetypeConfig);
|
|
29
34
|
const fileData = yield (0, repoFilesystemFacts_1.collectRepoFileData)(repoPath, archetypeConfig);
|
|
@@ -34,16 +39,46 @@ function analyzeCodebase(repoPath_1) {
|
|
|
34
39
|
fileContent: config_1.REPO_GLOBAL_CHECK
|
|
35
40
|
});
|
|
36
41
|
const { minimumDependencyVersions, standardStructure } = archetypeConfig.config;
|
|
37
|
-
|
|
42
|
+
let openaiSystemPrompt;
|
|
43
|
+
if (process.env.OPENAI_API_KEY) {
|
|
44
|
+
openaiSystemPrompt = yield (0, openaiAnalysisFacts_1.collectOpenaiAnalysisFacts)(fileData);
|
|
45
|
+
}
|
|
38
46
|
const engine = new json_rules_engine_1.Engine([], { replaceFactsInEventParams: true, allowUndefinedFacts: true });
|
|
47
|
+
// Get GitHub repository URL
|
|
48
|
+
let repoUrl = '';
|
|
49
|
+
try {
|
|
50
|
+
repoUrl = (0, child_process_1.execSync)('git config --get remote.origin.url', { cwd: repoPath }).toString().trim();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
logger_1.logger.warn('Unable to get GitHub repository URL');
|
|
54
|
+
}
|
|
55
|
+
// Get host information
|
|
56
|
+
const hostInfo = {
|
|
57
|
+
platform: os_1.default.platform(),
|
|
58
|
+
release: os_1.default.release(),
|
|
59
|
+
type: os_1.default.type(),
|
|
60
|
+
arch: os_1.default.arch(),
|
|
61
|
+
cpus: os_1.default.cpus().length,
|
|
62
|
+
totalMemory: os_1.default.totalmem(),
|
|
63
|
+
freeMemory: os_1.default.freemem()
|
|
64
|
+
};
|
|
65
|
+
// Get user information
|
|
66
|
+
const userInfo = {
|
|
67
|
+
username: os_1.default.userInfo().username,
|
|
68
|
+
homedir: os_1.default.userInfo().homedir,
|
|
69
|
+
shell: os_1.default.userInfo().shell
|
|
70
|
+
};
|
|
39
71
|
// Send telemetry for analysis start
|
|
40
72
|
yield (0, telemetry_1.sendTelemetry)({
|
|
41
73
|
eventType: 'analysisStart',
|
|
42
74
|
metadata: {
|
|
43
75
|
archetype,
|
|
44
76
|
repoPath,
|
|
77
|
+
repoUrl,
|
|
45
78
|
fileCount: fileData.length,
|
|
46
|
-
configServer: configServer || 'none'
|
|
79
|
+
configServer: configServer || 'none',
|
|
80
|
+
hostInfo,
|
|
81
|
+
userInfo
|
|
47
82
|
},
|
|
48
83
|
timestamp: new Date().toISOString()
|
|
49
84
|
});
|
|
@@ -59,7 +94,7 @@ function analyzeCodebase(repoPath_1) {
|
|
|
59
94
|
});
|
|
60
95
|
// Add rules to engine
|
|
61
96
|
logger_1.logger.info(`### loading json rules..`);
|
|
62
|
-
const rules = yield (0, rules_1.loadRules)(archetype, archetypeConfig.rules, configManager.configServer, logger_1.logPrefix);
|
|
97
|
+
const rules = yield (0, rules_1.loadRules)(archetype, archetypeConfig.rules, configManager.configServer, logger_1.logPrefix, configManager.localConfigPath);
|
|
63
98
|
logger_1.logger.debug(rules);
|
|
64
99
|
rules.forEach((rule) => {
|
|
65
100
|
try {
|
|
@@ -154,9 +189,12 @@ function analyzeCodebase(repoPath_1) {
|
|
|
154
189
|
metadata: {
|
|
155
190
|
archetype,
|
|
156
191
|
repoPath,
|
|
192
|
+
repoUrl,
|
|
157
193
|
fileCount: fileData.length,
|
|
158
194
|
failureCount: failures.length,
|
|
159
|
-
fatalityCount: fatalities.length
|
|
195
|
+
fatalityCount: fatalities.length,
|
|
196
|
+
hostInfo,
|
|
197
|
+
userInfo
|
|
160
198
|
},
|
|
161
199
|
timestamp: new Date().toISOString()
|
|
162
200
|
});
|
package/dist/core/engine.test.js
CHANGED
|
@@ -80,7 +80,7 @@ describe('analyzeCodebase', () => {
|
|
|
80
80
|
const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath', 'node-fullstack');
|
|
81
81
|
expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
|
|
82
82
|
expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalledWith(expect.any(Object));
|
|
83
|
-
expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
|
|
83
|
+
expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
|
|
84
84
|
expect(operators_1.loadOperators).toHaveBeenCalledWith(['mockOperator']);
|
|
85
85
|
expect(facts_1.loadFacts).toHaveBeenCalledWith(['mockFact']);
|
|
86
86
|
expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
|
|
@@ -113,7 +113,7 @@ describe('analyzeCodebase', () => {
|
|
|
113
113
|
const results = yield (0, engine_1.analyzeCodebase)('mockRepoPath', 'node-fullstack');
|
|
114
114
|
expect(repoFilesystemFacts_1.collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
|
|
115
115
|
expect(repoDependencyFacts_1.getDependencyVersionFacts).toHaveBeenCalled();
|
|
116
|
-
expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
|
|
116
|
+
expect(rules_1.loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
|
|
117
117
|
expect(operators_1.loadOperators).toHaveBeenCalledWith(['mockOperator']);
|
|
118
118
|
expect(facts_1.loadFacts).toHaveBeenCalledWith(['mockFact']);
|
|
119
119
|
expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
|
package/dist/rules/index.js
CHANGED
|
@@ -40,7 +40,7 @@ const logger_1 = require("../utils/logger");
|
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const axios_1 = __importDefault(require("axios"));
|
|
43
|
-
function loadRules(archetype, ruleNames, configServer, logPrefix) {
|
|
43
|
+
function loadRules(archetype, ruleNames, configServer, logPrefix, localConfigPath) {
|
|
44
44
|
return __awaiter(this, void 0, void 0, function* () {
|
|
45
45
|
const ruleProperties = [];
|
|
46
46
|
for (const ruleName of ruleNames) {
|
|
@@ -63,6 +63,12 @@ function loadRules(archetype, ruleNames, configServer, logPrefix) {
|
|
|
63
63
|
rule = yield loadLocalRule(ruleName);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
else if (localConfigPath) {
|
|
67
|
+
rule = yield loadLocalConfigRule(ruleName, localConfigPath);
|
|
68
|
+
}
|
|
69
|
+
else if (localConfigPath) {
|
|
70
|
+
rule = yield loadLocalConfigRule(ruleName, localConfigPath);
|
|
71
|
+
}
|
|
66
72
|
else {
|
|
67
73
|
rule = yield loadLocalRule(ruleName);
|
|
68
74
|
}
|
|
@@ -95,3 +101,22 @@ function loadLocalRule(ruleName) {
|
|
|
95
101
|
return null;
|
|
96
102
|
});
|
|
97
103
|
}
|
|
104
|
+
function loadLocalConfigRule(ruleName, localConfigPath) {
|
|
105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
const fileName = `${ruleName}-rule.json`;
|
|
107
|
+
const filePath = path.join(localConfigPath, 'rules', fileName);
|
|
108
|
+
if (!fileName.startsWith('openai') || (process.env.OPENAI_API_KEY && fileName.startsWith('openai'))) {
|
|
109
|
+
try {
|
|
110
|
+
logger_1.logger.debug(`Loading local config rule file: ${filePath}`);
|
|
111
|
+
const fileContent = yield fs.promises.readFile(filePath, 'utf8');
|
|
112
|
+
return JSON.parse(fileContent);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger_1.logger.error(`Error loading local config rule file: ${fileName}`);
|
|
116
|
+
logger_1.logger.error(error);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
});
|
|
122
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
26
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
27
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -16,12 +39,15 @@ exports.ConfigManager = exports.REPO_GLOBAL_CHECK = void 0;
|
|
|
16
39
|
const axios_1 = __importDefault(require("axios"));
|
|
17
40
|
const logger_1 = require("../utils/logger");
|
|
18
41
|
const archetypes_1 = require("../archetypes");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
19
44
|
exports.REPO_GLOBAL_CHECK = 'REPO_GLOBAL_CHECK';
|
|
20
45
|
class ConfigManager {
|
|
21
46
|
constructor() {
|
|
22
47
|
this.config = archetypes_1.archetypes['node-fullstack'];
|
|
23
48
|
this.rules = [];
|
|
24
49
|
this.configServer = '';
|
|
50
|
+
this.localConfigPath = '';
|
|
25
51
|
}
|
|
26
52
|
static getInstance() {
|
|
27
53
|
if (!ConfigManager.instance) {
|
|
@@ -30,10 +56,26 @@ class ConfigManager {
|
|
|
30
56
|
return ConfigManager.instance;
|
|
31
57
|
}
|
|
32
58
|
initialize() {
|
|
33
|
-
return __awaiter(this, arguments, void 0, function* (archetype = 'node-fullstack', configServer) {
|
|
59
|
+
return __awaiter(this, arguments, void 0, function* (archetype = 'node-fullstack', configServer, localConfigPath) {
|
|
34
60
|
this.config = archetypes_1.archetypes[archetype] || archetypes_1.archetypes['node-fullstack'];
|
|
35
61
|
this.configServer = configServer || '';
|
|
62
|
+
this.localConfigPath = localConfigPath || '';
|
|
36
63
|
logger_1.logger.debug(`Initializing config manager for archetype: ${archetype}`);
|
|
64
|
+
if (this.localConfigPath) {
|
|
65
|
+
try {
|
|
66
|
+
const localConfig = yield this.loadLocalConfig(archetype);
|
|
67
|
+
this.config = Object.assign(Object.assign({}, this.config), localConfig);
|
|
68
|
+
logger_1.logger.debug(`Local config loaded successfully ${JSON.stringify(this.config)}`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof Error) {
|
|
72
|
+
logger_1.logger.error(`Error loading local config: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
logger_1.logger.error('Error loading local config: Unknown error');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
37
79
|
if (this.configServer) {
|
|
38
80
|
try {
|
|
39
81
|
const configUrl = `${this.configServer}/archetypes/${archetype}`;
|
|
@@ -53,6 +95,25 @@ class ConfigManager {
|
|
|
53
95
|
}
|
|
54
96
|
});
|
|
55
97
|
}
|
|
98
|
+
loadLocalConfig(archetype) {
|
|
99
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
const configPath = path.join(this.localConfigPath, `${archetype}.json`);
|
|
101
|
+
try {
|
|
102
|
+
const configContent = yield fs.promises.readFile(configPath, 'utf8');
|
|
103
|
+
const localConfig = JSON.parse(configContent);
|
|
104
|
+
return Object.assign(Object.assign({}, archetypes_1.archetypes[archetype]), localConfig);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof Error) {
|
|
108
|
+
logger_1.logger.error(`Error loading local config: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
logger_1.logger.error('Error loading local config: Unknown error');
|
|
112
|
+
}
|
|
113
|
+
return archetypes_1.archetypes[archetype];
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
56
117
|
getConfig() {
|
|
57
118
|
return this.config;
|
|
58
119
|
}
|
package/dist/utils/telemetry.js
CHANGED
|
@@ -15,15 +15,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.sendTelemetry = sendTelemetry;
|
|
16
16
|
const axios_1 = __importDefault(require("axios"));
|
|
17
17
|
const logger_1 = require("./logger");
|
|
18
|
-
const
|
|
18
|
+
const cli_1 = require("../core/cli");
|
|
19
|
+
const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || (cli_1.options.configServer ? `${cli_1.options.configServer}/telemetry` : '');
|
|
19
20
|
function sendTelemetry(event) {
|
|
20
21
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22
|
+
if (!TELEMETRY_ENDPOINT) {
|
|
23
|
+
logger_1.logger.debug('Telemetry endpoint not set. Skipping telemetry');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
21
26
|
try {
|
|
22
|
-
yield axios_1.default.post(TELEMETRY_ENDPOINT, event
|
|
27
|
+
yield axios_1.default.post(TELEMETRY_ENDPOINT, event, {
|
|
28
|
+
timeout: 5000, // 5 seconds timeout
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'User-Agent': 'x-fidelity-telemetry'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
23
34
|
logger_1.logger.debug(`Telemetry sent: ${JSON.stringify(event)}`);
|
|
24
35
|
}
|
|
25
36
|
catch (error) {
|
|
26
|
-
|
|
37
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
38
|
+
logger_1.logger.error(`Failed to send telemetry: ${error.message}`);
|
|
39
|
+
if (error.response) {
|
|
40
|
+
logger_1.logger.error(`Response status: ${error.response.status}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logger_1.logger.error(`Failed to send telemetry: ${error}`);
|
|
45
|
+
}
|
|
27
46
|
}
|
|
28
47
|
});
|
|
29
48
|
}
|
package/package.json
CHANGED
package/src/archetypes/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ArchetypeConfig } from '../types/typeDefs';
|
|
|
2
2
|
|
|
3
3
|
export const archetypes: Record<string, ArchetypeConfig> = {
|
|
4
4
|
'node-fullstack': {
|
|
5
|
-
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global'],
|
|
5
|
+
rules: ['sensitiveLogging-iterative', 'outdatedFramework-global', 'noDatabases-iterative', 'nonStandardDirectoryStructure-global', 'openaiAnalysisTop5-global', 'openaiAnalysisA11y-global', 'yarnLockfileCheck-global'],
|
|
6
6
|
operators: ['fileContains', 'outdatedFramework', 'nonStandardDirectoryStructure', 'openaiAnalysisHighSeverity'],
|
|
7
7
|
facts: ['repoFilesystemFacts', 'repoDependencyFacts', 'openaiAnalysisFacts'],
|
|
8
8
|
config: {
|
|
@@ -15,10 +15,6 @@ export const archetypes: Record<string, ArchetypeConfig> = {
|
|
|
15
15
|
frontend: null,
|
|
16
16
|
server: null
|
|
17
17
|
}
|
|
18
|
-
// app: {
|
|
19
|
-
// frontend: 'required',
|
|
20
|
-
// server: 'required'
|
|
21
|
-
// }
|
|
22
18
|
},
|
|
23
19
|
blacklistPatterns: [
|
|
24
20
|
'.*\\/\\..*', // dot files
|
package/src/core/cli.ts
CHANGED
|
@@ -8,11 +8,12 @@ if (!logger || typeof logger.info !== 'function') {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
program
|
|
11
|
-
.option("-d, --dir <directory>", "The checkout directory to analyze", ".")
|
|
12
|
-
.option("-a, --archetype <archetype>", "The archetype to use for analysis", "node-fullstack")
|
|
11
|
+
.option("-d, --dir <directory>", "The checkout directory to analyze (default: current directory)", ".")
|
|
12
|
+
.option("-a, --archetype <archetype>", "The archetype to use for analysis (default: node-fullstack)", "node-fullstack")
|
|
13
13
|
.option("-c, --configServer <configServer>", "The config server URL for fetching remote archetype configurations and rules")
|
|
14
|
-
.option("-m, --mode <mode>", "Run mode: 'cli' or 'server'", "cli")
|
|
15
|
-
.option("-p, --port <port>", "Port number for server mode", "8888")
|
|
14
|
+
.option("-m, --mode <mode>", "Run mode: 'cli' or 'server' (default: cli)", "cli")
|
|
15
|
+
.option("-p, --port <port>", "Port number for server mode (default: 8888)", "8888")
|
|
16
|
+
.option("-l, --local-config <path>", "Path to local archetype config and rules");
|
|
16
17
|
|
|
17
18
|
program.parse();
|
|
18
19
|
|
|
@@ -36,6 +37,7 @@ directory: ${process.env.PWD}/${options.dir}
|
|
|
36
37
|
configServer: ${options.configServer ? options.configServer : 'none'}
|
|
37
38
|
mode: ${options.mode}
|
|
38
39
|
port: ${options.mode === 'server' ? options.port : 'N/A'}
|
|
40
|
+
local-config: ${options.localConfig ? options.localConfig : 'none'}
|
|
39
41
|
for available options run: xfidelity --help
|
|
40
42
|
=====================================`);
|
|
41
43
|
|
package/src/core/engine.test.ts
CHANGED
|
@@ -76,7 +76,7 @@ describe('analyzeCodebase', () => {
|
|
|
76
76
|
|
|
77
77
|
expect(collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
|
|
78
78
|
expect(getDependencyVersionFacts).toHaveBeenCalledWith(expect.any(Object));
|
|
79
|
-
expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
|
|
79
|
+
expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
|
|
80
80
|
expect(loadOperators).toHaveBeenCalledWith(['mockOperator']);
|
|
81
81
|
expect(loadFacts).toHaveBeenCalledWith(['mockFact']);
|
|
82
82
|
expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
|
|
@@ -114,7 +114,7 @@ describe('analyzeCodebase', () => {
|
|
|
114
114
|
|
|
115
115
|
expect(collectRepoFileData).toHaveBeenCalledWith('mockRepoPath', expect.any(Object));
|
|
116
116
|
expect(getDependencyVersionFacts).toHaveBeenCalled();
|
|
117
|
-
expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String));
|
|
117
|
+
expect(loadRules).toHaveBeenCalledWith('node-fullstack', ['mockRule'], '', expect.any(String), undefined);
|
|
118
118
|
expect(loadOperators).toHaveBeenCalledWith(['mockOperator']);
|
|
119
119
|
expect(loadFacts).toHaveBeenCalledWith(['mockFact']);
|
|
120
120
|
expect(engineRunMock).toHaveBeenCalledTimes(mockFileData.length);
|
package/src/core/engine.ts
CHANGED
|
@@ -9,10 +9,12 @@ import { loadFacts } from '../facts';
|
|
|
9
9
|
import { loadRules } from '../rules';
|
|
10
10
|
import { ConfigManager, REPO_GLOBAL_CHECK } from '../utils/config';
|
|
11
11
|
import { sendTelemetry } from '../utils/telemetry';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import os from 'os';
|
|
12
14
|
|
|
13
|
-
async function analyzeCodebase(repoPath: string, archetype: string = 'node-fullstack', configServer: string = ''): Promise<any[]> {
|
|
15
|
+
async function analyzeCodebase(repoPath: string, archetype: string = 'node-fullstack', configServer: string = '', localConfigPath: string = ''): Promise<any[]> {
|
|
14
16
|
const configManager = ConfigManager.getInstance();
|
|
15
|
-
await configManager.initialize(archetype, configServer);
|
|
17
|
+
await configManager.initialize(archetype, configServer, localConfigPath);
|
|
16
18
|
const archetypeConfig = configManager.getConfig();
|
|
17
19
|
|
|
18
20
|
const installedDependencyVersions = await getDependencyVersionFacts(archetypeConfig);
|
|
@@ -26,18 +28,51 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
|
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
const { minimumDependencyVersions, standardStructure } = archetypeConfig.config;
|
|
29
|
-
|
|
31
|
+
|
|
32
|
+
let openaiSystemPrompt;
|
|
33
|
+
if (process.env.OPENAI_API_KEY) {
|
|
34
|
+
openaiSystemPrompt = await collectOpenaiAnalysisFacts(fileData);
|
|
35
|
+
}
|
|
30
36
|
|
|
31
37
|
const engine = new Engine([], { replaceFactsInEventParams: true, allowUndefinedFacts: true });
|
|
32
38
|
|
|
39
|
+
// Get GitHub repository URL
|
|
40
|
+
let repoUrl = '';
|
|
41
|
+
try {
|
|
42
|
+
repoUrl = execSync('git config --get remote.origin.url', { cwd: repoPath }).toString().trim();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.warn('Unable to get GitHub repository URL');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get host information
|
|
48
|
+
const hostInfo = {
|
|
49
|
+
platform: os.platform(),
|
|
50
|
+
release: os.release(),
|
|
51
|
+
type: os.type(),
|
|
52
|
+
arch: os.arch(),
|
|
53
|
+
cpus: os.cpus().length,
|
|
54
|
+
totalMemory: os.totalmem(),
|
|
55
|
+
freeMemory: os.freemem()
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Get user information
|
|
59
|
+
const userInfo = {
|
|
60
|
+
username: os.userInfo().username,
|
|
61
|
+
homedir: os.userInfo().homedir,
|
|
62
|
+
shell: os.userInfo().shell
|
|
63
|
+
};
|
|
64
|
+
|
|
33
65
|
// Send telemetry for analysis start
|
|
34
66
|
await sendTelemetry({
|
|
35
67
|
eventType: 'analysisStart',
|
|
36
68
|
metadata: {
|
|
37
69
|
archetype,
|
|
38
70
|
repoPath,
|
|
71
|
+
repoUrl,
|
|
39
72
|
fileCount: fileData.length,
|
|
40
|
-
configServer: configServer || 'none'
|
|
73
|
+
configServer: configServer || 'none',
|
|
74
|
+
hostInfo,
|
|
75
|
+
userInfo
|
|
41
76
|
},
|
|
42
77
|
timestamp: new Date().toISOString()
|
|
43
78
|
});
|
|
@@ -54,7 +89,7 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
|
|
|
54
89
|
|
|
55
90
|
// Add rules to engine
|
|
56
91
|
logger.info(`### loading json rules..`);
|
|
57
|
-
const rules: RuleProperties[] = await loadRules(archetype, archetypeConfig.rules, configManager.configServer, logPrefix);
|
|
92
|
+
const rules: RuleProperties[] = await loadRules(archetype, archetypeConfig.rules, configManager.configServer, logPrefix, configManager.localConfigPath);
|
|
58
93
|
logger.debug(rules);
|
|
59
94
|
|
|
60
95
|
rules.forEach((rule) => {
|
|
@@ -165,9 +200,12 @@ async function analyzeCodebase(repoPath: string, archetype: string = 'node-fulls
|
|
|
165
200
|
metadata: {
|
|
166
201
|
archetype,
|
|
167
202
|
repoPath,
|
|
203
|
+
repoUrl,
|
|
168
204
|
fileCount: fileData.length,
|
|
169
205
|
failureCount: failures.length,
|
|
170
|
-
fatalityCount: fatalities.length
|
|
206
|
+
fatalityCount: fatalities.length,
|
|
207
|
+
hostInfo,
|
|
208
|
+
userInfo
|
|
171
209
|
},
|
|
172
210
|
timestamp: new Date().toISOString()
|
|
173
211
|
});
|
package/src/rules/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import * as path from 'path';
|
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import { options } from "../core/cli";
|
|
7
7
|
|
|
8
|
-
async function loadRules(archetype:
|
|
8
|
+
async function loadRules(archetype: string, ruleNames: string[], configServer?: string, logPrefix?: string, localConfigPath?: string): Promise<RuleProperties[]> {
|
|
9
9
|
|
|
10
10
|
const ruleProperties: RuleProperties[] = [];
|
|
11
11
|
|
|
@@ -28,6 +28,10 @@ async function loadRules(archetype: any, ruleNames: string[], configServer?: str
|
|
|
28
28
|
// If remote fetch fails, fall back to local file
|
|
29
29
|
rule = await loadLocalRule(ruleName);
|
|
30
30
|
}
|
|
31
|
+
} else if (localConfigPath) {
|
|
32
|
+
rule = await loadLocalConfigRule(ruleName, localConfigPath);
|
|
33
|
+
} else if (localConfigPath) {
|
|
34
|
+
rule = await loadLocalConfigRule(ruleName, localConfigPath);
|
|
31
35
|
} else {
|
|
32
36
|
rule = await loadLocalRule(ruleName);
|
|
33
37
|
}
|
|
@@ -63,3 +67,20 @@ async function loadLocalRule(ruleName: string): Promise<RuleProperties | null> {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
export { loadRules };
|
|
70
|
+
async function loadLocalConfigRule(ruleName: string, localConfigPath: string): Promise<RuleProperties | null> {
|
|
71
|
+
const fileName = `${ruleName}-rule.json`;
|
|
72
|
+
const filePath = path.join(localConfigPath, 'rules', fileName);
|
|
73
|
+
|
|
74
|
+
if (!fileName.startsWith('openai') || (process.env.OPENAI_API_KEY && fileName.startsWith('openai'))) {
|
|
75
|
+
try {
|
|
76
|
+
logger.debug(`Loading local config rule file: ${filePath}`);
|
|
77
|
+
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
78
|
+
return JSON.parse(fileContent);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.error(`Error loading local config rule file: ${fileName}`);
|
|
81
|
+
logger.error(error);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { logger, logPrefix } from "../utils/logger";
|
|
|
4
4
|
import { ArchetypeConfig } from "../types/typeDefs";
|
|
5
5
|
import { archetypes } from "../archetypes";
|
|
6
6
|
import { loadRules } from "../rules";
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
7
9
|
|
|
8
10
|
export const REPO_GLOBAL_CHECK = 'REPO_GLOBAL_CHECK';
|
|
9
11
|
|
|
@@ -12,11 +14,13 @@ export class ConfigManager {
|
|
|
12
14
|
private config: ArchetypeConfig;
|
|
13
15
|
private rules: any[];
|
|
14
16
|
public configServer: string;
|
|
17
|
+
public localConfigPath: string;
|
|
15
18
|
|
|
16
19
|
private constructor() {
|
|
17
20
|
this.config = archetypes['node-fullstack'];
|
|
18
21
|
this.rules = [];
|
|
19
22
|
this.configServer = '';
|
|
23
|
+
this.localConfigPath = '';
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
public static getInstance(): ConfigManager {
|
|
@@ -26,10 +30,29 @@ export class ConfigManager {
|
|
|
26
30
|
return ConfigManager.instance;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
public async initialize(archetype: string = 'node-fullstack', configServer?: string): Promise<void> {
|
|
33
|
+
public async initialize(archetype: string = 'node-fullstack', configServer?: string, localConfigPath?: string): Promise<void> {
|
|
30
34
|
this.config = archetypes[archetype] || archetypes['node-fullstack'];
|
|
31
35
|
this.configServer = configServer || '';
|
|
36
|
+
this.localConfigPath = localConfigPath || '';
|
|
32
37
|
logger.debug(`Initializing config manager for archetype: ${archetype}`);
|
|
38
|
+
|
|
39
|
+
if (this.localConfigPath) {
|
|
40
|
+
try {
|
|
41
|
+
const localConfig = await this.loadLocalConfig(archetype);
|
|
42
|
+
this.config = {
|
|
43
|
+
...this.config,
|
|
44
|
+
...localConfig
|
|
45
|
+
};
|
|
46
|
+
logger.debug(`Local config loaded successfully ${JSON.stringify(this.config)}`);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
logger.error(`Error loading local config: ${error.message}`);
|
|
50
|
+
} else {
|
|
51
|
+
logger.error('Error loading local config: Unknown error');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
if (this.configServer) {
|
|
34
57
|
try {
|
|
35
58
|
const configUrl = `${this.configServer}/archetypes/${archetype}`;
|
|
@@ -48,7 +71,25 @@ export class ConfigManager {
|
|
|
48
71
|
}
|
|
49
72
|
}
|
|
50
73
|
}
|
|
74
|
+
}
|
|
51
75
|
|
|
76
|
+
private async loadLocalConfig(archetype: string): Promise<ArchetypeConfig> {
|
|
77
|
+
const configPath = path.join(this.localConfigPath, `${archetype}.json`);
|
|
78
|
+
try {
|
|
79
|
+
const configContent = await fs.promises.readFile(configPath, 'utf8');
|
|
80
|
+
const localConfig = JSON.parse(configContent);
|
|
81
|
+
return {
|
|
82
|
+
...archetypes[archetype],
|
|
83
|
+
...localConfig
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof Error) {
|
|
87
|
+
logger.error(`Error loading local config: ${error.message}`);
|
|
88
|
+
} else {
|
|
89
|
+
logger.error('Error loading local config: Unknown error');
|
|
90
|
+
}
|
|
91
|
+
return archetypes[archetype];
|
|
92
|
+
}
|
|
52
93
|
}
|
|
53
94
|
|
|
54
95
|
public getConfig(): ArchetypeConfig {
|
package/src/utils/telemetry.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { logger } from './logger';
|
|
3
|
+
import { options } from "../core/cli";
|
|
3
4
|
|
|
4
5
|
interface TelemetryEvent {
|
|
5
6
|
eventType: string;
|
|
@@ -11,13 +12,30 @@ interface TelemetryEvent {
|
|
|
11
12
|
timestamp: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT ||
|
|
15
|
+
const TELEMETRY_ENDPOINT = process.env.TELEMETRY_ENDPOINT || (options.configServer ? `${options.configServer}/telemetry` : '');
|
|
15
16
|
|
|
16
17
|
export async function sendTelemetry(event: TelemetryEvent): Promise<void> {
|
|
18
|
+
if (!TELEMETRY_ENDPOINT) {
|
|
19
|
+
logger.debug('Telemetry endpoint not set. Skipping telemetry');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
17
22
|
try {
|
|
18
|
-
await axios.post(TELEMETRY_ENDPOINT, event
|
|
23
|
+
await axios.post(TELEMETRY_ENDPOINT, event, {
|
|
24
|
+
timeout: 5000, // 5 seconds timeout
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'User-Agent': 'x-fidelity-telemetry'
|
|
28
|
+
}
|
|
29
|
+
});
|
|
19
30
|
logger.debug(`Telemetry sent: ${JSON.stringify(event)}`);
|
|
20
31
|
} catch (error) {
|
|
21
|
-
|
|
32
|
+
if (axios.isAxiosError(error)) {
|
|
33
|
+
logger.error(`Failed to send telemetry: ${error.message}`);
|
|
34
|
+
if (error.response) {
|
|
35
|
+
logger.error(`Response status: ${error.response.status}`);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
logger.error(`Failed to send telemetry: ${error}`);
|
|
39
|
+
}
|
|
22
40
|
}
|
|
23
41
|
}
|