pumuki-ast-hooks 5.5.50 → 5.5.52
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/docs/MCP_SERVERS.md +16 -2
- package/docs/RELEASE_NOTES.md +34 -0
- package/hooks/git-status-monitor.ts +0 -5
- package/hooks/notify-macos.ts +0 -1
- package/hooks/pre-tool-use-evidence-validator.ts +0 -1
- package/package.json +2 -2
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +96 -0
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +2 -4
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +2 -20
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +9 -139
- package/scripts/hooks-system/application/services/installation/mcp/McpGlobalConfigCleaner.js +49 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpProjectConfigWriter.js +59 -0
- package/scripts/hooks-system/application/services/installation/mcp/McpServerConfigBuilder.js +103 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +1 -13
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +17 -9
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +2 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDChecks.js +385 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDRules.js +38 -408
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +397 -34
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMChecks.js +408 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMRules.js +36 -442
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenExtractor.js +146 -0
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenParser.js +22 -190
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenRunner.js +62 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +398 -1
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +0 -16
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +14 -25
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +20 -76
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +0 -350
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const env = require('../../../../config/env.js');
|
|
6
|
+
|
|
7
|
+
function slugifyId(input) {
|
|
8
|
+
return String(input || '')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-+|-+$/g, '')
|
|
13
|
+
.slice(0, 48);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class McpServerConfigBuilder {
|
|
17
|
+
constructor(targetRoot, hookSystemRoot, logger = null) {
|
|
18
|
+
this.targetRoot = targetRoot;
|
|
19
|
+
this.hookSystemRoot = hookSystemRoot;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
build() {
|
|
24
|
+
const serverId = this.computeServerIdForRepo(this.targetRoot);
|
|
25
|
+
const entrypoint = this.resolveAutomationEntrypoint();
|
|
26
|
+
const nodePath = this.resolveNodeBinary();
|
|
27
|
+
|
|
28
|
+
const mcpConfig = {
|
|
29
|
+
mcpServers: {
|
|
30
|
+
[serverId]: {
|
|
31
|
+
command: nodePath,
|
|
32
|
+
args: [entrypoint],
|
|
33
|
+
env: {
|
|
34
|
+
REPO_ROOT: this.targetRoot,
|
|
35
|
+
AUTO_COMMIT_ENABLED: 'false',
|
|
36
|
+
AUTO_PUSH_ENABLED: 'false',
|
|
37
|
+
AUTO_PR_ENABLED: 'false'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { serverId, mcpConfig };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
resolveAutomationEntrypoint() {
|
|
47
|
+
const candidates = [
|
|
48
|
+
this.hookSystemRoot
|
|
49
|
+
? path.join(this.hookSystemRoot, 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
|
|
50
|
+
: null,
|
|
51
|
+
path.join(this.targetRoot, 'scripts', 'hooks-system', 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
|
|
52
|
+
].filter(Boolean);
|
|
53
|
+
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger?.warn?.('MCP_ENTRYPOINT_CHECK_FAILED', { candidate, error: error.message });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return path.join(this.targetRoot, 'scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resolveNodeBinary() {
|
|
66
|
+
let nodePath = process.execPath;
|
|
67
|
+
if (nodePath && fs.existsSync(nodePath)) return nodePath;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return execSync('which node', { encoding: 'utf-8' }).trim();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (process.env.DEBUG) {
|
|
73
|
+
process.stderr.write(`[MCP] which node failed: ${error && error.message ? error.message : String(error)}\n`);
|
|
74
|
+
}
|
|
75
|
+
return 'node';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
computeServerIdForRepo(repoRoot) {
|
|
80
|
+
const legacyServerId = 'ast-intelligence-automation';
|
|
81
|
+
const forced = (env.get('MCP_SERVER_ID', '') || '').trim();
|
|
82
|
+
if (forced.length > 0) return forced;
|
|
83
|
+
|
|
84
|
+
const repoName = path.basename(repoRoot || process.cwd());
|
|
85
|
+
const slug = slugifyId(repoName) || 'repo';
|
|
86
|
+
const fp = this.computeRepoFingerprint(repoRoot);
|
|
87
|
+
return `${legacyServerId}-${slug}-${fp}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
computeRepoFingerprint(repoRoot) {
|
|
91
|
+
try {
|
|
92
|
+
const real = fs.realpathSync(repoRoot);
|
|
93
|
+
return crypto.createHash('sha1').update(real).digest('hex').slice(0, 8);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (process.env.DEBUG) {
|
|
96
|
+
process.stderr.write(`[MCP] computeRepoFingerprint fallback: ${error && error.message ? error.message : String(error)}\n`);
|
|
97
|
+
}
|
|
98
|
+
return crypto.createHash('sha1').update(String(repoRoot || '')).digest('hex').slice(0, 8);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = McpServerConfigBuilder;
|
|
@@ -45,18 +45,6 @@ function getRepoRoot() {
|
|
|
45
45
|
*/
|
|
46
46
|
function shouldIgnore(file) {
|
|
47
47
|
const p = file.replace(/\\/g, "/");
|
|
48
|
-
try {
|
|
49
|
-
const configPaths = loadExclusions()?.exclusions?.paths;
|
|
50
|
-
if (configPaths && typeof configPaths === 'object') {
|
|
51
|
-
for (const [key, enabled] of Object.entries(configPaths)) {
|
|
52
|
-
if (enabled && p.includes(key)) return true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (process.env.DEBUG) {
|
|
57
|
-
console.debug(`[ast-core] Failed to load exclusions for shouldIgnore: ${error.message || String(error)}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
48
|
if (p.includes("node_modules/")) return true;
|
|
61
49
|
if (p.includes("/.next/")) return true;
|
|
62
50
|
if (p.includes("/dist/")) return true;
|
|
@@ -137,7 +125,7 @@ let exclusionsConfig = null;
|
|
|
137
125
|
function loadExclusions() {
|
|
138
126
|
if (exclusionsConfig) return exclusionsConfig;
|
|
139
127
|
try {
|
|
140
|
-
const configPath = path.join(
|
|
128
|
+
const configPath = path.join(__dirname, '../../config/ast-exclusions.json');
|
|
141
129
|
if (fs.existsSync(configPath)) {
|
|
142
130
|
exclusionsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
143
131
|
}
|
|
@@ -22,7 +22,7 @@ function formatLocalTimestamp(date = new Date()) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const astModulesPath = __dirname;
|
|
25
|
-
const { createProject, platformOf, mapToLevel
|
|
25
|
+
const { createProject, platformOf, mapToLevel } = require(path.join(astModulesPath, "ast-core"));
|
|
26
26
|
const MacOSNotificationAdapter = require(path.join(__dirname, '../adapters/MacOSNotificationAdapter'));
|
|
27
27
|
const { runBackendIntelligence } = require(path.join(astModulesPath, "backend/ast-backend"));
|
|
28
28
|
const { runFrontendIntelligence } = require(path.join(astModulesPath, "frontend/ast-frontend"));
|
|
@@ -138,6 +138,8 @@ function runProjectHardcodedThresholdAudit(root, allFiles, findings) {
|
|
|
138
138
|
if (p.includes('/build/')) return true;
|
|
139
139
|
if (p.includes('/coverage/')) return true;
|
|
140
140
|
if (p.includes('/.audit_tmp/')) return true;
|
|
141
|
+
if (p.includes('/infrastructure/ast/')) return true;
|
|
142
|
+
if (p.includes('/scripts/hooks-system/')) return true;
|
|
141
143
|
return false;
|
|
142
144
|
};
|
|
143
145
|
|
|
@@ -704,7 +706,6 @@ function listSourceFiles(root) {
|
|
|
704
706
|
*/
|
|
705
707
|
function shouldIgnore(file) {
|
|
706
708
|
const p = file.replace(/\\/g, "/");
|
|
707
|
-
if (typeof coreShouldIgnore === 'function' && coreShouldIgnore(p)) return true;
|
|
708
709
|
if (p.includes("node_modules/")) return true;
|
|
709
710
|
if (p.includes("/.cursor/")) return true;
|
|
710
711
|
if (/\.bak/i.test(p)) return true;
|
|
@@ -126,10 +126,13 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
// NO excluir archivos AST - la librería debe auto-auditarse
|
|
129
|
+
if (isTestFile(filePath)) return;
|
|
130
|
+
|
|
129
131
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
130
132
|
const className = cls.getName() || '';
|
|
131
133
|
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
132
|
-
|
|
134
|
+
const isTestClass = /Spec$|Test$|Mock/.test(className);
|
|
135
|
+
if (isValueObject || isTestClass) return;
|
|
133
136
|
|
|
134
137
|
const methodsCount = cls.getMethods().length;
|
|
135
138
|
const propertiesCount = cls.getProperties().length;
|
|
@@ -207,6 +210,9 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
207
210
|
|
|
208
211
|
if (platformOf(filePath) !== "backend") return;
|
|
209
212
|
|
|
213
|
+
const normalizedBackendPath = filePath.replace(/\\/g, '/');
|
|
214
|
+
const isRealBackendAppFile = normalizedBackendPath.includes('/apps/backend/') || normalizedBackendPath.includes('apps/backend/');
|
|
215
|
+
|
|
210
216
|
// NO excluir archivos AST - la librería debe auto-auditarse para detectar God classes masivas
|
|
211
217
|
|
|
212
218
|
const fullText = sf.getFullText();
|
|
@@ -272,7 +278,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
272
278
|
fullTextUpper.includes("ConfigService") ||
|
|
273
279
|
usesEnvHelper;
|
|
274
280
|
const hasConfigUsage = /process\.env\b|ConfigService|env\.get(Bool|Number)?\(|\bconfig\s*[\.\[]/i.test(sf.getFullText());
|
|
275
|
-
if (!hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
|
|
281
|
+
if (isRealBackendAppFile && !hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
|
|
276
282
|
pushFinding("backend.config.missing_env_separation", "info", sf, sf, "Missing environment-specific configuration - consider NODE_ENV or ConfigService", findings);
|
|
277
283
|
}
|
|
278
284
|
|
|
@@ -287,6 +293,10 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
287
293
|
analyzeGodClasses(sf, findings, { SyntaxKind, pushFinding, godClassBaseline });
|
|
288
294
|
}
|
|
289
295
|
|
|
296
|
+
if (!isRealBackendAppFile) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
290
300
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
291
301
|
const name = cls.getName();
|
|
292
302
|
if (name && /Entity|Model|Domain/.test(name)) {
|
|
@@ -349,16 +359,12 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
349
359
|
}
|
|
350
360
|
});
|
|
351
361
|
|
|
352
|
-
const filePathNormalizedForMetrics = filePath.replace(/\\/g, "/");
|
|
353
|
-
const filePathNormalizedForMetricsLower = filePathNormalizedForMetrics.toLowerCase();
|
|
354
|
-
const isInternalAstToolingFileForMetrics = filePathNormalizedForMetricsLower.includes("/infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("/scripts/hooks-system/infrastructure/ast/");
|
|
355
|
-
|
|
356
362
|
const fullTextLower = fullText.toLowerCase();
|
|
357
363
|
const hasMetrics = fullTextLower.includes("micrometer") || fullTextLower.includes("prometheus") ||
|
|
358
364
|
fullTextLower.includes("actuator") || fullTextLower.includes("metrics");
|
|
359
365
|
const looksLikeServiceOrController = fullTextLower.includes("controller") || fullTextLower.includes("service");
|
|
360
366
|
|
|
361
|
-
if (
|
|
367
|
+
if (isRealBackendAppFile && !hasMetrics && looksLikeServiceOrController) {
|
|
362
368
|
pushFinding("backend.metrics.missing_prometheus", "info", sf, sf, "Missing application metrics - consider Spring Boot Actuator or Micrometer for monitoring", findings);
|
|
363
369
|
}
|
|
364
370
|
|
|
@@ -478,7 +484,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
478
484
|
sf.getDescendantsOfKind(SyntaxKind.ThrowStatement).forEach((throwStmt) => {
|
|
479
485
|
const expr = throwStmt.getExpression();
|
|
480
486
|
if (!expr) return;
|
|
481
|
-
const exprText =
|
|
487
|
+
const exprText = throwStmt.getExpression().getText();
|
|
482
488
|
if (exprText.includes("Error(") || exprText.includes("Exception(")) {
|
|
483
489
|
const isCustom = exprText.includes("Exception") &&
|
|
484
490
|
(exprText.includes("Validation") ||
|
|
@@ -486,7 +492,9 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
486
492
|
exprText.includes("Unauthorized") ||
|
|
487
493
|
exprText.includes("Forbidden"));
|
|
488
494
|
if (!isCustom) {
|
|
489
|
-
|
|
495
|
+
if (isRealBackendAppFile) {
|
|
496
|
+
pushFinding("backend.error.custom_exceptions", "info", sf, throwStmt, "Generic Error/Exception thrown - create custom exception classes for better error handling", findings);
|
|
497
|
+
}
|
|
490
498
|
}
|
|
491
499
|
}
|
|
492
500
|
});
|
|
@@ -7,7 +7,8 @@ function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godC
|
|
|
7
7
|
sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
8
8
|
const className = cls.getName() || '';
|
|
9
9
|
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
10
|
-
|
|
10
|
+
const isTestClass = /Spec$|Test$|Mock/.test(className);
|
|
11
|
+
if (isValueObject || isTestClass) return;
|
|
11
12
|
|
|
12
13
|
const methodsCount = cls.getMethods().length;
|
|
13
14
|
const propertiesCount = cls.getProperties().length;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const glob = require('glob');
|
|
4
|
+
|
|
5
|
+
function checkFastfileExists(ctx) {
|
|
6
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
7
|
+
if (!fs.existsSync(fastfilePath)) {
|
|
8
|
+
ctx.pushFinding(ctx.findings, {
|
|
9
|
+
ruleId: 'ios.cicd.missing_fastfile',
|
|
10
|
+
severity: 'medium',
|
|
11
|
+
message: 'Proyecto iOS sin Fastfile. Fastlane automatiza builds, tests y deployments.',
|
|
12
|
+
filePath: 'PROJECT_ROOT',
|
|
13
|
+
line: 1,
|
|
14
|
+
suggestion: `Inicializar Fastlane:
|
|
15
|
+
|
|
16
|
+
fastlane init
|
|
17
|
+
|
|
18
|
+
Esto crea:
|
|
19
|
+
- fastlane/Fastfile
|
|
20
|
+
- fastlane/Appfile
|
|
21
|
+
- fastlane/Matchfile`
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
checkFastfileLanes(ctx, fastfilePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function checkFastfileLanes(ctx, fastfilePath) {
|
|
29
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
30
|
+
const requiredLanes = ['test', 'beta', 'release'];
|
|
31
|
+
const existingLanes = content.match(/lane\s+:(\w+)/g)?.map(l => l.match(/:(\w+)/)?.[1]) || [];
|
|
32
|
+
const missingLanes = requiredLanes.filter(lane => !existingLanes.includes(lane));
|
|
33
|
+
|
|
34
|
+
if (missingLanes.length > 0) {
|
|
35
|
+
ctx.pushFinding(ctx.findings, {
|
|
36
|
+
ruleId: 'ios.cicd.fastfile_missing_lanes',
|
|
37
|
+
severity: 'medium',
|
|
38
|
+
message: `Fastfile sin lanes esenciales: ${missingLanes.join(', ')}`,
|
|
39
|
+
filePath: fastfilePath,
|
|
40
|
+
line: 1,
|
|
41
|
+
suggestion: `Añadir lanes:
|
|
42
|
+
|
|
43
|
+
lane :test do
|
|
44
|
+
scan
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
lane :beta do
|
|
48
|
+
build_app
|
|
49
|
+
upload_to_testflight
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
lane :release do
|
|
53
|
+
build_app
|
|
54
|
+
upload_to_app_store
|
|
55
|
+
end`
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!content.includes('increment_build_number') && !content.includes('increment_version_number')) {
|
|
60
|
+
ctx.pushFinding(ctx.findings, {
|
|
61
|
+
ruleId: 'ios.cicd.missing_version_increment',
|
|
62
|
+
severity: 'medium',
|
|
63
|
+
message: 'Fastfile sin incremento automático de versión/build.',
|
|
64
|
+
filePath: fastfilePath,
|
|
65
|
+
line: 1
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkGitHubActionsWorkflow(ctx) {
|
|
71
|
+
const workflowPath = path.join(ctx.projectRoot, '.github/workflows');
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(workflowPath)) {
|
|
74
|
+
ctx.pushFinding(ctx.findings, {
|
|
75
|
+
ruleId: 'ios.cicd.missing_github_actions',
|
|
76
|
+
severity: 'low',
|
|
77
|
+
message: 'Proyecto sin GitHub Actions workflows. Automatizar CI/CD.',
|
|
78
|
+
filePath: 'PROJECT_ROOT',
|
|
79
|
+
line: 1,
|
|
80
|
+
suggestion: 'Crear .github/workflows/ios-ci.yml para tests automáticos'
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const workflows = glob.sync('*.yml', { cwd: workflowPath, absolute: true });
|
|
86
|
+
const iosWorkflows = workflows.filter(w => {
|
|
87
|
+
const content = fs.readFileSync(w, 'utf-8');
|
|
88
|
+
return content.includes('macos') || content.includes('ios') || content.includes('xcodebuild');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (iosWorkflows.length === 0) {
|
|
92
|
+
ctx.pushFinding(ctx.findings, {
|
|
93
|
+
ruleId: 'ios.cicd.no_ios_workflow',
|
|
94
|
+
severity: 'medium',
|
|
95
|
+
message: 'GitHub Actions sin workflow de iOS.',
|
|
96
|
+
filePath: '.github/workflows/',
|
|
97
|
+
line: 1
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
iosWorkflows.forEach(workflow => {
|
|
103
|
+
const content = fs.readFileSync(workflow, 'utf-8');
|
|
104
|
+
|
|
105
|
+
if (!content.includes('xcodebuild test')) {
|
|
106
|
+
ctx.pushFinding(ctx.findings, {
|
|
107
|
+
ruleId: 'ios.cicd.workflow_missing_tests',
|
|
108
|
+
severity: 'high',
|
|
109
|
+
message: 'Workflow de iOS sin tests automáticos.',
|
|
110
|
+
filePath: workflow,
|
|
111
|
+
line: 1
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!content.includes('fastlane')) {
|
|
116
|
+
ctx.pushFinding(ctx.findings, {
|
|
117
|
+
ruleId: 'ios.cicd.workflow_without_fastlane',
|
|
118
|
+
severity: 'low',
|
|
119
|
+
message: 'Workflow sin Fastlane. Considerar para simplificar pipeline.',
|
|
120
|
+
filePath: workflow,
|
|
121
|
+
line: 1
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function checkTestFlightConfiguration(ctx) {
|
|
128
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
129
|
+
if (!fs.existsSync(fastfilePath)) return;
|
|
130
|
+
|
|
131
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
132
|
+
if (content.includes('beta') && !content.includes('upload_to_testflight')) {
|
|
133
|
+
ctx.pushFinding(ctx.findings, {
|
|
134
|
+
ruleId: 'ios.cicd.beta_without_testflight',
|
|
135
|
+
severity: 'medium',
|
|
136
|
+
message: 'Lane beta sin upload_to_testflight. Automatizar distribución.',
|
|
137
|
+
filePath: fastfilePath,
|
|
138
|
+
line: 1
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (content.includes('upload_to_testflight') && !content.includes('changelog')) {
|
|
143
|
+
ctx.pushFinding(ctx.findings, {
|
|
144
|
+
ruleId: 'ios.cicd.testflight_without_changelog',
|
|
145
|
+
severity: 'low',
|
|
146
|
+
message: 'TestFlight sin changelog. Añadir notas de release.',
|
|
147
|
+
filePath: fastfilePath,
|
|
148
|
+
line: 1
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkBuildConfiguration(ctx) {
|
|
154
|
+
const projectFiles = glob.sync('**/*.xcodeproj/project.pbxproj', {
|
|
155
|
+
cwd: ctx.projectRoot,
|
|
156
|
+
absolute: true
|
|
157
|
+
});
|
|
158
|
+
if (projectFiles.length === 0) return;
|
|
159
|
+
|
|
160
|
+
projectFiles.forEach(projectFile => {
|
|
161
|
+
const content = fs.readFileSync(projectFile, 'utf-8');
|
|
162
|
+
const configurations = content.match(/buildConfiguration\s*=\s*(\w+)/g) || [];
|
|
163
|
+
const hasDebug = configurations.some(c => c.includes('Debug'));
|
|
164
|
+
const hasRelease = configurations.some(c => c.includes('Release'));
|
|
165
|
+
const hasStaging = configurations.some(c => c.includes('Staging'));
|
|
166
|
+
|
|
167
|
+
if (!hasStaging && (hasDebug && hasRelease)) {
|
|
168
|
+
ctx.pushFinding(ctx.findings, {
|
|
169
|
+
ruleId: 'ios.cicd.missing_staging_config',
|
|
170
|
+
severity: 'low',
|
|
171
|
+
message: 'Sin configuración Staging. Útil para testing pre-production.',
|
|
172
|
+
filePath: projectFile,
|
|
173
|
+
line: 1
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function checkCertificateManagement(ctx) {
|
|
180
|
+
const matchfilePath = path.join(ctx.projectRoot, 'fastlane/Matchfile');
|
|
181
|
+
if (fs.existsSync(matchfilePath)) return;
|
|
182
|
+
|
|
183
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
184
|
+
if (fs.existsSync(fastfilePath)) {
|
|
185
|
+
ctx.pushFinding(ctx.findings, {
|
|
186
|
+
ruleId: 'ios.cicd.missing_match_config',
|
|
187
|
+
severity: 'medium',
|
|
188
|
+
message: 'Fastlane sin Match para gestión de certificados.',
|
|
189
|
+
filePath: 'fastlane/',
|
|
190
|
+
line: 1,
|
|
191
|
+
suggestion: 'fastlane match init para gestionar certificados en equipo'
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function checkCodeSigningConfiguration(ctx) {
|
|
197
|
+
const projectFiles = glob.sync('**/*.xcodeproj/project.pbxproj', {
|
|
198
|
+
cwd: ctx.projectRoot,
|
|
199
|
+
absolute: true
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
projectFiles.forEach(projectFile => {
|
|
203
|
+
const content = fs.readFileSync(projectFile, 'utf-8');
|
|
204
|
+
if (content.includes('CODE_SIGN_STYLE = Manual')) {
|
|
205
|
+
ctx.pushFinding(ctx.findings, {
|
|
206
|
+
ruleId: 'ios.cicd.manual_code_signing',
|
|
207
|
+
severity: 'low',
|
|
208
|
+
message: 'Code signing manual. Considerar Automatic o Match para CI/CD.',
|
|
209
|
+
filePath: projectFile,
|
|
210
|
+
line: 1
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function checkAutomatedTesting(ctx) {
|
|
217
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
218
|
+
if (!fs.existsSync(fastfilePath)) return;
|
|
219
|
+
|
|
220
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
221
|
+
if (!content.includes('scan') && !content.includes('run_tests')) {
|
|
222
|
+
ctx.pushFinding(ctx.findings, {
|
|
223
|
+
ruleId: 'ios.cicd.no_automated_tests',
|
|
224
|
+
severity: 'high',
|
|
225
|
+
message: 'Fastlane sin tests automatizados. Añadir scan action.',
|
|
226
|
+
filePath: fastfilePath,
|
|
227
|
+
line: 1,
|
|
228
|
+
suggestion: `lane :test do
|
|
229
|
+
scan(scheme: "MyApp")
|
|
230
|
+
end`
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function checkVersionBumping(ctx) {
|
|
236
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
237
|
+
if (!fs.existsSync(fastfilePath)) return;
|
|
238
|
+
|
|
239
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
240
|
+
if (content.includes('upload_to') && !content.includes('increment_build_number')) {
|
|
241
|
+
ctx.pushFinding(ctx.findings, {
|
|
242
|
+
ruleId: 'ios.cicd.missing_build_increment',
|
|
243
|
+
severity: 'medium',
|
|
244
|
+
message: 'Deployment sin incremento automático de build number.',
|
|
245
|
+
filePath: fastfilePath,
|
|
246
|
+
line: 1
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function checkReleaseNotes(ctx) {
|
|
252
|
+
const changelogPath = path.join(ctx.projectRoot, 'CHANGELOG.md');
|
|
253
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
254
|
+
if (!fs.existsSync(fastfilePath) || fs.existsSync(changelogPath)) return;
|
|
255
|
+
|
|
256
|
+
ctx.pushFinding(ctx.findings, {
|
|
257
|
+
ruleId: 'ios.cicd.missing_changelog',
|
|
258
|
+
severity: 'low',
|
|
259
|
+
message: 'Proyecto sin CHANGELOG.md. Documentar cambios para releases.',
|
|
260
|
+
filePath: 'PROJECT_ROOT',
|
|
261
|
+
line: 1
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function checkSlackNotifications(ctx) {
|
|
266
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
267
|
+
if (!fs.existsSync(fastfilePath)) return;
|
|
268
|
+
|
|
269
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
270
|
+
if (content.includes('upload_to') && !content.includes('slack')) {
|
|
271
|
+
ctx.pushFinding(ctx.findings, {
|
|
272
|
+
ruleId: 'ios.cicd.missing_notifications',
|
|
273
|
+
severity: 'low',
|
|
274
|
+
message: 'Deployment sin notificaciones. Añadir Slack para avisar al equipo.',
|
|
275
|
+
filePath: fastfilePath,
|
|
276
|
+
line: 1
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function checkMatchConfiguration(ctx) {
|
|
282
|
+
const matchfilePath = path.join(ctx.projectRoot, 'fastlane/Matchfile');
|
|
283
|
+
if (!fs.existsSync(matchfilePath)) return;
|
|
284
|
+
|
|
285
|
+
const content = fs.readFileSync(matchfilePath, 'utf-8');
|
|
286
|
+
if (!content.includes('git_url')) {
|
|
287
|
+
ctx.pushFinding(ctx.findings, {
|
|
288
|
+
ruleId: 'ios.cicd.match_missing_git_url',
|
|
289
|
+
severity: 'high',
|
|
290
|
+
message: 'Matchfile sin git_url. Match requiere repositorio para certificados.',
|
|
291
|
+
filePath: matchfilePath,
|
|
292
|
+
line: 1
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function checkGymConfiguration(ctx) {
|
|
298
|
+
const gymfilePath = path.join(ctx.projectRoot, 'fastlane/Gymfile');
|
|
299
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
300
|
+
if (!fs.existsSync(fastfilePath) || fs.existsSync(gymfilePath)) return;
|
|
301
|
+
|
|
302
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
303
|
+
if (content.includes('build_app') || content.includes('gym')) {
|
|
304
|
+
ctx.pushFinding(ctx.findings, {
|
|
305
|
+
ruleId: 'ios.cicd.missing_gymfile',
|
|
306
|
+
severity: 'low',
|
|
307
|
+
message: 'build_app sin Gymfile. Considerar para centralizar configuración de build.',
|
|
308
|
+
filePath: 'fastlane/',
|
|
309
|
+
line: 1
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function checkScanConfiguration(ctx) {
|
|
315
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
316
|
+
if (!fs.existsSync(fastfilePath)) return;
|
|
317
|
+
|
|
318
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
319
|
+
if (content.includes('scan') && !content.includes('code_coverage')) {
|
|
320
|
+
ctx.pushFinding(ctx.findings, {
|
|
321
|
+
ruleId: 'ios.cicd.scan_without_coverage',
|
|
322
|
+
severity: 'low',
|
|
323
|
+
message: 'scan sin code_coverage: true. Activar para métricas.',
|
|
324
|
+
filePath: fastfilePath,
|
|
325
|
+
line: 1,
|
|
326
|
+
suggestion: 'scan(code_coverage: true, scheme: "MyApp")'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function checkPilotConfiguration(ctx) {
|
|
332
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
333
|
+
if (!fs.existsSync(fastfilePath)) return;
|
|
334
|
+
|
|
335
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
336
|
+
if (!content.includes('pilot') && !content.includes('upload_to_testflight')) return;
|
|
337
|
+
|
|
338
|
+
if (!content.includes('changelog') && !content.includes('whats_new')) {
|
|
339
|
+
ctx.pushFinding(ctx.findings, {
|
|
340
|
+
ruleId: 'ios.cicd.pilot_missing_changelog',
|
|
341
|
+
severity: 'low',
|
|
342
|
+
message: 'TestFlight upload sin changelog/whats_new.',
|
|
343
|
+
filePath: fastfilePath,
|
|
344
|
+
line: 1
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function checkAppStoreMetadata(ctx) {
|
|
350
|
+
const metadataPath = path.join(ctx.projectRoot, 'fastlane/metadata');
|
|
351
|
+
if (fs.existsSync(metadataPath)) return;
|
|
352
|
+
|
|
353
|
+
const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
|
|
354
|
+
if (!fs.existsSync(fastfilePath)) return;
|
|
355
|
+
|
|
356
|
+
const content = fs.readFileSync(fastfilePath, 'utf-8');
|
|
357
|
+
if (content.includes('upload_to_app_store') || content.includes('deliver')) {
|
|
358
|
+
ctx.pushFinding(ctx.findings, {
|
|
359
|
+
ruleId: 'ios.cicd.missing_metadata',
|
|
360
|
+
severity: 'low',
|
|
361
|
+
message: 'Upload a App Store sin metadata/ folder. Versionar descripciones y screenshots.',
|
|
362
|
+
filePath: 'fastlane/',
|
|
363
|
+
line: 1,
|
|
364
|
+
suggestion: 'fastlane deliver init para crear estructura metadata/'
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = {
|
|
370
|
+
checkFastfileExists,
|
|
371
|
+
checkGitHubActionsWorkflow,
|
|
372
|
+
checkTestFlightConfiguration,
|
|
373
|
+
checkBuildConfiguration,
|
|
374
|
+
checkCertificateManagement,
|
|
375
|
+
checkCodeSigningConfiguration,
|
|
376
|
+
checkAutomatedTesting,
|
|
377
|
+
checkVersionBumping,
|
|
378
|
+
checkReleaseNotes,
|
|
379
|
+
checkSlackNotifications,
|
|
380
|
+
checkMatchConfiguration,
|
|
381
|
+
checkGymConfiguration,
|
|
382
|
+
checkScanConfiguration,
|
|
383
|
+
checkPilotConfiguration,
|
|
384
|
+
checkAppStoreMetadata,
|
|
385
|
+
};
|