@gokiteam/goki-dev 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +478 -0
- package/bin/goki-dev.js +452 -0
- package/bin/mcp-server.js +16 -0
- package/bin/secrets-cli.js +302 -0
- package/cli/ComposeOverrideGenerator.js +226 -0
- package/cli/ComposeParser.js +73 -0
- package/cli/ConfigGenerator.js +304 -0
- package/cli/ConfigManager.js +46 -0
- package/cli/DatabaseManager.js +94 -0
- package/cli/DevToolsChecker.js +21 -0
- package/cli/DevToolsDir.js +66 -0
- package/cli/DevToolsManager.js +451 -0
- package/cli/DockerManager.js +138 -0
- package/cli/FunctionManager.js +95 -0
- package/cli/HttpProxyRewriter.js +91 -0
- package/cli/Logger.js +10 -0
- package/cli/McpConfigManager.js +123 -0
- package/cli/NgrokManager.js +431 -0
- package/cli/ProjectCLI.js +2322 -0
- package/cli/PubSubManager.js +129 -0
- package/cli/SnapshotManager.js +88 -0
- package/cli/UiFormatter.js +292 -0
- package/cli/WebhookUrlRewriter.js +32 -0
- package/cli/secrets/BiometricAuth.js +125 -0
- package/cli/secrets/SecretInjector.js +47 -0
- package/cli/secrets/SecretsConfig.js +141 -0
- package/cli/secrets/SecretsDoctor.js +384 -0
- package/cli/secrets/SecretsManager.js +255 -0
- package/client/dist/client.d.ts +332 -0
- package/client/dist/client.js +507 -0
- package/client/dist/helpers.d.ts +62 -0
- package/client/dist/helpers.js +122 -0
- package/client/dist/index.d.ts +59 -0
- package/client/dist/index.js +78 -0
- package/client/dist/package.json +1 -0
- package/client/dist/types.d.ts +280 -0
- package/client/dist/types.js +7 -0
- package/config.development +46 -0
- package/config.test +18 -0
- package/guidelines/CodingStyleGuideline.md +148 -0
- package/guidelines/CommentingGuideline.md +10 -0
- package/guidelines/HttpApiImplementationGuideline.md +137 -0
- package/guidelines/NamingGuideline.md +182 -0
- package/package.json +138 -0
- package/patterns/api/[collectionName]/Controllers.md +62 -0
- package/patterns/api/[collectionName]/Logic.md +154 -0
- package/patterns/api/[collectionName]/Permissions.md +81 -0
- package/patterns/api/[collectionName]/Router.md +83 -0
- package/patterns/api/[collectionName]/Schemas.md +197 -0
- package/patterns/configs/Patterns.md +7 -0
- package/patterns/enums/Patterns.md +24 -0
- package/patterns/errorHandling/Patterns.md +185 -0
- package/patterns/testing/Patterns.md +232 -0
- package/src/Server.js +238 -0
- package/src/api/dashboard/Controllers.js +9 -0
- package/src/api/dashboard/Logic.js +76 -0
- package/src/api/dashboard/Router.js +11 -0
- package/src/api/dashboard/Schemas.js +47 -0
- package/src/api/data/Controllers.js +26 -0
- package/src/api/data/Logic.js +188 -0
- package/src/api/data/Router.js +16 -0
- package/src/api/docker/Controllers.js +33 -0
- package/src/api/docker/Logic.js +268 -0
- package/src/api/docker/Router.js +15 -0
- package/src/api/docker/Schemas.js +80 -0
- package/src/api/docs/Controllers.js +15 -0
- package/src/api/docs/Logic.js +85 -0
- package/src/api/docs/Router.js +12 -0
- package/src/api/export/Controllers.js +30 -0
- package/src/api/export/Logic.js +143 -0
- package/src/api/export/Router.js +18 -0
- package/src/api/export/Schemas.js +104 -0
- package/src/api/firestore/Controllers.js +152 -0
- package/src/api/firestore/Logic.js +474 -0
- package/src/api/firestore/Router.js +23 -0
- package/src/api/functions/Controllers.js +261 -0
- package/src/api/functions/Logic.js +710 -0
- package/src/api/functions/Router.js +50 -0
- package/src/api/functions/Schemas.js +193 -0
- package/src/api/gateway/Controllers.js +72 -0
- package/src/api/gateway/Logic.js +74 -0
- package/src/api/gateway/Router.js +10 -0
- package/src/api/gateway/Schemas.js +19 -0
- package/src/api/health/Controllers.js +14 -0
- package/src/api/health/Logic.js +24 -0
- package/src/api/health/Router.js +12 -0
- package/src/api/httpTraffic/Controllers.js +29 -0
- package/src/api/httpTraffic/Logic.js +33 -0
- package/src/api/httpTraffic/Router.js +9 -0
- package/src/api/httpTraffic/Schemas.js +23 -0
- package/src/api/logging/Controllers.js +80 -0
- package/src/api/logging/Logic.js +461 -0
- package/src/api/logging/Router.js +24 -0
- package/src/api/logging/Schemas.js +43 -0
- package/src/api/mqtt/Controllers.js +17 -0
- package/src/api/mqtt/Logic.js +66 -0
- package/src/api/mqtt/Router.js +12 -0
- package/src/api/postgres/Controllers.js +97 -0
- package/src/api/postgres/Logic.js +221 -0
- package/src/api/postgres/Router.js +21 -0
- package/src/api/pubsub/Controllers.js +236 -0
- package/src/api/pubsub/Logic.js +732 -0
- package/src/api/pubsub/Router.js +41 -0
- package/src/api/pubsub/Schemas.js +355 -0
- package/src/api/redis/Controllers.js +63 -0
- package/src/api/redis/Logic.js +239 -0
- package/src/api/redis/Router.js +21 -0
- package/src/api/scheduler/Controllers.js +27 -0
- package/src/api/scheduler/Logic.js +49 -0
- package/src/api/scheduler/Router.js +16 -0
- package/src/api/services/Controllers.js +26 -0
- package/src/api/services/Logic.js +205 -0
- package/src/api/services/Router.js +14 -0
- package/src/api/services/Schemas.js +66 -0
- package/src/api/snapshots/Controllers.js +37 -0
- package/src/api/snapshots/Logic.js +797 -0
- package/src/api/snapshots/Router.js +15 -0
- package/src/api/snapshots/Schemas.js +23 -0
- package/src/api/webhooks/Controllers.js +49 -0
- package/src/api/webhooks/Logic.js +137 -0
- package/src/api/webhooks/Router.js +12 -0
- package/src/api/webhooks/Schemas.js +31 -0
- package/src/configs/Application.js +147 -0
- package/src/configs/Default.js +13 -0
- package/src/consumers/BlackboxLogsConsumer.js +235 -0
- package/src/consumers/DockerLogsConsumer.js +687 -0
- package/src/db/Tables.js +66 -0
- package/src/db/schemas/firestore.js +18 -0
- package/src/db/schemas/functions.js +65 -0
- package/src/db/schemas/httpTraffic.js +43 -0
- package/src/db/schemas/logging.js +74 -0
- package/src/db/schemas/migrations.js +64 -0
- package/src/db/schemas/mqtt.js +56 -0
- package/src/db/schemas/pubsub.js +90 -0
- package/src/db/schemas/pubsubRegistry.js +22 -0
- package/src/db/schemas/webhooks.js +28 -0
- package/src/emulation/awsiot/Controllers.js +91 -0
- package/src/emulation/awsiot/Logic.js +70 -0
- package/src/emulation/awsiot/Router.js +19 -0
- package/src/emulation/awsiot/Server.js +100 -0
- package/src/emulation/firestore/Server.js +136 -0
- package/src/emulation/logging/Controllers.js +212 -0
- package/src/emulation/logging/Logic.js +416 -0
- package/src/emulation/logging/Router.js +36 -0
- package/src/emulation/logging/Schemas.js +82 -0
- package/src/emulation/logging/Server.js +108 -0
- package/src/emulation/pubsub/Controllers.js +279 -0
- package/src/emulation/pubsub/DefaultTopics.js +162 -0
- package/src/emulation/pubsub/Logic.js +427 -0
- package/src/emulation/pubsub/README.md +309 -0
- package/src/emulation/pubsub/Router.js +33 -0
- package/src/emulation/pubsub/Server.js +104 -0
- package/src/emulation/pubsub/ShadowPoller.js +276 -0
- package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
- package/src/enums/ContainerNames.js +106 -0
- package/src/enums/ErrorReason.js +28 -0
- package/src/enums/FunctionStatuses.js +15 -0
- package/src/enums/FunctionTriggerTypes.js +15 -0
- package/src/enums/GatewayState.js +7 -0
- package/src/enums/ServiceNames.js +68 -0
- package/src/jobs/DatabaseMaintenance.js +184 -0
- package/src/jobs/MessageHistoryCleanup.js +152 -0
- package/src/mcp/ApiClient.js +25 -0
- package/src/mcp/Server.js +52 -0
- package/src/mcp/prompts/debugging.js +104 -0
- package/src/mcp/resources/platform.js +118 -0
- package/src/mcp/tools/data.js +84 -0
- package/src/mcp/tools/docker.js +166 -0
- package/src/mcp/tools/firestore.js +162 -0
- package/src/mcp/tools/functions.js +380 -0
- package/src/mcp/tools/httpTraffic.js +69 -0
- package/src/mcp/tools/logging.js +174 -0
- package/src/mcp/tools/mqtt.js +37 -0
- package/src/mcp/tools/postgres.js +130 -0
- package/src/mcp/tools/pubsub.js +316 -0
- package/src/mcp/tools/redis.js +146 -0
- package/src/mcp/tools/services.js +169 -0
- package/src/mcp/tools/snapshots.js +88 -0
- package/src/mcp/tools/webhooks.js +115 -0
- package/src/middleware/DevProxy.js +67 -0
- package/src/middleware/ErrorCatcher.js +35 -0
- package/src/middleware/HttpProxy.js +215 -0
- package/src/middleware/Reply.js +24 -0
- package/src/middleware/TraceId.js +9 -0
- package/src/middleware/WebhookProxy.js +234 -0
- package/src/protocols/mqtt/Broker.js +92 -0
- package/src/protocols/mqtt/Handlers.js +175 -0
- package/src/protocols/mqtt/PubSubBridge.js +162 -0
- package/src/protocols/mqtt/Server.js +116 -0
- package/src/runtime/FunctionRunner.js +179 -0
- package/src/services/AppGatewayService.js +582 -0
- package/src/singletons/FirestoreBroadcaster.js +367 -0
- package/src/singletons/FunctionTriggerDispatcher.js +456 -0
- package/src/singletons/FunctionsService.js +418 -0
- package/src/singletons/HttpProxy.js +224 -0
- package/src/singletons/LogBroadcaster.js +159 -0
- package/src/singletons/Logger.js +49 -0
- package/src/singletons/MemoryJsonStore.js +175 -0
- package/src/singletons/MessageBroadcaster.js +190 -0
- package/src/singletons/PostgresBroadcaster.js +367 -0
- package/src/singletons/PostgresClient.js +180 -0
- package/src/singletons/RedisClient.js +184 -0
- package/src/singletons/SqliteStore.js +480 -0
- package/src/singletons/TickService.js +151 -0
- package/src/singletons/WebhookProxy.js +223 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
|
|
4
|
+
export class SecretInjector {
|
|
5
|
+
constructor (secretsManager) {
|
|
6
|
+
this.secretsManager = secretsManager
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
formatEnv ({ format = 'shell', keys } = {}) {
|
|
10
|
+
const envMap = this.secretsManager.getEnvMap()
|
|
11
|
+
const filtered = this._filterKeys(envMap, keys)
|
|
12
|
+
if (format === 'json') {
|
|
13
|
+
return JSON.stringify(filtered, null, 2)
|
|
14
|
+
}
|
|
15
|
+
return Object.entries(filtered)
|
|
16
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
17
|
+
.join('\n')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async run (command, args = [], { verbose = false } = {}) {
|
|
21
|
+
const envMap = this.secretsManager.getEnvMap()
|
|
22
|
+
if (verbose) {
|
|
23
|
+
const keyList = Object.keys(envMap).join(', ')
|
|
24
|
+
console.log(chalk.green(`\u2713 ${Object.keys(envMap).length} secrets injected (${keyList})`))
|
|
25
|
+
}
|
|
26
|
+
const result = await execa(command, args, {
|
|
27
|
+
env: { ...process.env, ...envMap },
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
reject: false
|
|
30
|
+
})
|
|
31
|
+
if (result.exitCode !== 0) {
|
|
32
|
+
process.exit(result.exitCode)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_filterKeys (envMap, keys) {
|
|
37
|
+
if (!keys) return envMap
|
|
38
|
+
const keyList = Array.isArray(keys) ? keys : keys.split(',').map(k => k.trim())
|
|
39
|
+
const filtered = {}
|
|
40
|
+
for (const key of keyList) {
|
|
41
|
+
if (key in envMap) {
|
|
42
|
+
filtered[key] = envMap[key]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return filtered
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
|
|
5
|
+
export class SecretsConfig {
|
|
6
|
+
constructor (secretsManager) {
|
|
7
|
+
this.secretsManager = secretsManager
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async loadConfig (projectDir) {
|
|
11
|
+
const configPath = path.join(projectDir, '.dev-tools', 'config.js')
|
|
12
|
+
if (!fs.existsSync(configPath)) return null
|
|
13
|
+
try {
|
|
14
|
+
const mod = await import(configPath)
|
|
15
|
+
return mod.default?.secrets || null
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async setup () {
|
|
22
|
+
const projectDir = this.secretsManager.projectDir
|
|
23
|
+
if (!projectDir) {
|
|
24
|
+
console.log(chalk.blue(' \u2139 no project detected \u2014 nothing to set up'))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
const config = await this.loadConfig(projectDir)
|
|
28
|
+
if (!config) {
|
|
29
|
+
console.log(chalk.blue(' \u2139 no secrets defined in .dev-tools/config.js'))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const projectName = this.secretsManager.projectName
|
|
33
|
+
const entries = Object.entries(config)
|
|
34
|
+
console.log(chalk.blue(` \u2139 project: ${projectName} (from .dev-tools/config.js)`))
|
|
35
|
+
console.log()
|
|
36
|
+
console.log(` Checking ${entries.length} secrets...`)
|
|
37
|
+
const missing = []
|
|
38
|
+
for (const [key, def] of entries) {
|
|
39
|
+
const scope = def.scope || 'project'
|
|
40
|
+
const scopeLabel = scope === 'global' ? 'global' : `project`
|
|
41
|
+
const value = this.secretsManager.get(key)
|
|
42
|
+
if (value !== undefined) {
|
|
43
|
+
console.log(chalk.green(` \u2713 ${key} (${scopeLabel}) \u2014 already set`))
|
|
44
|
+
} else if (def.default !== undefined) {
|
|
45
|
+
console.log(chalk.green(` \u2713 ${key} (${scopeLabel}) \u2014 using default`))
|
|
46
|
+
await this.secretsManager.set(key, String(def.default), {
|
|
47
|
+
scope,
|
|
48
|
+
description: def.description
|
|
49
|
+
})
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.red(` \u2717 ${key} (${scopeLabel}) \u2014 missing`))
|
|
52
|
+
missing.push({ key, scope, description: def.description })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (missing.length > 0) {
|
|
56
|
+
console.log()
|
|
57
|
+
const inquirer = (await import('inquirer')).default
|
|
58
|
+
for (const { key, scope, description } of missing) {
|
|
59
|
+
const { value } = await inquirer.prompt([{
|
|
60
|
+
type: 'password',
|
|
61
|
+
name: 'value',
|
|
62
|
+
message: `${key}:`,
|
|
63
|
+
mask: '\u25CF'
|
|
64
|
+
}])
|
|
65
|
+
await this.secretsManager.set(key, value, { scope, description })
|
|
66
|
+
const scopeLabel = scope === 'global'
|
|
67
|
+
? 'global'
|
|
68
|
+
: `project: ${projectName}`
|
|
69
|
+
console.log(chalk.green(` \u2713 set ${key} (${scopeLabel})`))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
console.log()
|
|
73
|
+
console.log(chalk.green(` \u2713 all ${entries.length} secrets configured`))
|
|
74
|
+
// Scan for plaintext credentials and offer to fix
|
|
75
|
+
await this._offerDoctorFix()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async _offerDoctorFix () {
|
|
79
|
+
const { SecretsDoctor } = await import('./SecretsDoctor.js')
|
|
80
|
+
const projectDir = this.secretsManager.projectDir || process.cwd()
|
|
81
|
+
const doctor = new SecretsDoctor(projectDir)
|
|
82
|
+
const findings = await doctor.scan()
|
|
83
|
+
if (findings.length === 0) return
|
|
84
|
+
console.log()
|
|
85
|
+
const label = findings.length === 1 ? 'plaintext credential' : 'plaintext credentials'
|
|
86
|
+
console.log(` ${chalk.yellow('\u26a0')} Found ${chalk.bold(findings.length)} ${label} in project files`)
|
|
87
|
+
const inquirer = (await import('inquirer')).default
|
|
88
|
+
const { fix } = await inquirer.prompt([{
|
|
89
|
+
type: 'confirm',
|
|
90
|
+
name: 'fix',
|
|
91
|
+
message: 'Fix plaintext credentials now?',
|
|
92
|
+
default: true
|
|
93
|
+
}])
|
|
94
|
+
if (!fix) {
|
|
95
|
+
console.log(chalk.dim(' \u2192 Run `goki-secrets doctor --fix` anytime'))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
await doctor.interactiveFix(findings, this.secretsManager)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async check () {
|
|
102
|
+
const projectDir = this.secretsManager.projectDir
|
|
103
|
+
if (!projectDir) {
|
|
104
|
+
console.log(chalk.blue(' \u2139 no project detected \u2014 nothing to check'))
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
const config = await this.loadConfig(projectDir)
|
|
108
|
+
if (!config) {
|
|
109
|
+
console.log(chalk.blue(' \u2139 no secrets defined in .dev-tools/config.js'))
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
const projectName = this.secretsManager.projectName
|
|
113
|
+
const entries = Object.entries(config)
|
|
114
|
+
let missingCount = 0
|
|
115
|
+
for (const [key, def] of entries) {
|
|
116
|
+
const scope = def.scope || 'project'
|
|
117
|
+
const scopeLabel = scope === 'global'
|
|
118
|
+
? 'global'
|
|
119
|
+
: `project: ${projectName}`
|
|
120
|
+
const value = this.secretsManager.get(key)
|
|
121
|
+
if (value !== undefined) {
|
|
122
|
+
console.log(chalk.green(` \u2713 ${key} (${scopeLabel})`))
|
|
123
|
+
} else if (def.default !== undefined) {
|
|
124
|
+
console.log(chalk.green(` \u2713 ${key} (${scopeLabel}) \u2014 default`))
|
|
125
|
+
} else if (def.required) {
|
|
126
|
+
console.log(chalk.red(` \u2717 ${key} (${scopeLabel}) \u2014 missing`))
|
|
127
|
+
missingCount++
|
|
128
|
+
} else {
|
|
129
|
+
console.log(chalk.gray(` - ${key} (${scopeLabel}) \u2014 not set (optional)`))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log()
|
|
133
|
+
if (missingCount > 0) {
|
|
134
|
+
const label = missingCount === 1 ? 'required secret' : 'required secrets'
|
|
135
|
+
console.log(chalk.red(` \u2717 ${missingCount} ${label} missing. Run: goki-secrets setup`))
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
console.log(chalk.green(' \u2713 all secrets configured'))
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
const SENSITIVE_PATTERNS = /^(.*_)?(PASSWORD|SECRET|API_KEY|AUTH_TOKEN|PRIVATE_KEY|NPM_TOKEN|ACCESS_TOKEN|REFRESH_TOKEN|CLIENT_SECRET)$/i
|
|
7
|
+
|
|
8
|
+
const ENV_FILES = ['.env', '.env.local', '.env.development', '.env.production', '.env.test']
|
|
9
|
+
const CONFIG_FILES = ['config.development', 'config.production', 'config.test', 'config.staging']
|
|
10
|
+
const COMPOSE_FILES = [
|
|
11
|
+
'docker-compose.yml',
|
|
12
|
+
'docker-compose.yaml',
|
|
13
|
+
'docker-compose.override.yml',
|
|
14
|
+
'docker-compose.services.yml'
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function isPlaceholder (value) {
|
|
18
|
+
if (!value || value.trim() === '') return true
|
|
19
|
+
if (/^\$\{.+\}$/.test(value)) return true
|
|
20
|
+
if (/^<.+>$/.test(value)) return true
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function maskValue (value) {
|
|
25
|
+
if (!value) return '****'
|
|
26
|
+
if (value.length <= 4) return '****'
|
|
27
|
+
return value.substring(0, 4) + '****'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveAbsPath (projectDir, displayPath) {
|
|
31
|
+
if (displayPath === '~/.npmrc') return path.join(os.homedir(), '.npmrc')
|
|
32
|
+
return path.join(projectDir, displayPath)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SecretsDoctor {
|
|
36
|
+
constructor (projectDir) {
|
|
37
|
+
this._projectDir = path.resolve(projectDir)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async scan () {
|
|
41
|
+
const findings = []
|
|
42
|
+
this._scanNpmrc(findings)
|
|
43
|
+
this._scanEnvFiles(findings)
|
|
44
|
+
this._scanConfigFiles(findings)
|
|
45
|
+
this._scanComposeFiles(findings)
|
|
46
|
+
return findings
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
report (findings) {
|
|
50
|
+
console.log()
|
|
51
|
+
console.log(' Scanning for plaintext credentials...')
|
|
52
|
+
console.log()
|
|
53
|
+
if (findings.length === 0) {
|
|
54
|
+
console.log(` ${chalk.green('✓')} No plaintext credentials found`)
|
|
55
|
+
console.log()
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
for (const finding of findings) {
|
|
59
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.white(`${finding.file}:${finding.line}`)} — ${finding.message}`)
|
|
60
|
+
console.log(` ${chalk.dim('→')} ${chalk.cyan(finding.fix)}`)
|
|
61
|
+
console.log()
|
|
62
|
+
}
|
|
63
|
+
const label = findings.length === 1 ? 'plaintext credential' : 'plaintext credentials'
|
|
64
|
+
console.log(` Found ${chalk.yellow(findings.length)} ${label}`)
|
|
65
|
+
console.log()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fixFinding (finding) {
|
|
69
|
+
const absPath = resolveAbsPath(this._projectDir, finding.file)
|
|
70
|
+
const content = fs.readFileSync(absPath, 'utf-8')
|
|
71
|
+
const lines = content.split('\n')
|
|
72
|
+
const lineIdx = finding.line - 1
|
|
73
|
+
if (lineIdx < 0 || lineIdx >= lines.length) return
|
|
74
|
+
const line = lines[lineIdx]
|
|
75
|
+
switch (finding.type) {
|
|
76
|
+
case 'npm_token':
|
|
77
|
+
lines[lineIdx] = line.replace(/_authToken=.+/, `_authToken=\${${finding.secretKey}}`)
|
|
78
|
+
break
|
|
79
|
+
case 'env_secret':
|
|
80
|
+
lines[lineIdx] = `# ${finding.secretKey}= # managed by goki-secrets`
|
|
81
|
+
break
|
|
82
|
+
case 'config_secret':
|
|
83
|
+
lines[lineIdx] = line.replace(
|
|
84
|
+
/([A-Z_][A-Z0-9_]*)[\s]*[=:][\s]*.+/,
|
|
85
|
+
`# $1= # managed by goki-secrets`
|
|
86
|
+
)
|
|
87
|
+
break
|
|
88
|
+
case 'compose_secret': {
|
|
89
|
+
const listMatch = line.match(/^(\s*-\s*)[A-Z_][A-Z0-9_]*=.+$/)
|
|
90
|
+
const mapMatch = line.match(/^(\s*)[A-Z_][A-Z0-9_]*\s*:\s*.+$/)
|
|
91
|
+
if (listMatch) {
|
|
92
|
+
lines[lineIdx] = `${listMatch[1]}${finding.secretKey}`
|
|
93
|
+
} else if (mapMatch) {
|
|
94
|
+
lines[lineIdx] = `${mapMatch[1]}- ${finding.secretKey}`
|
|
95
|
+
}
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(absPath, lines.join('\n'))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async interactiveFix (findings, secretsManager) {
|
|
103
|
+
const inquirer = (await import('inquirer')).default
|
|
104
|
+
let migratedKeys = 0
|
|
105
|
+
let fixedFiles = 0
|
|
106
|
+
let skippedKeys = 0
|
|
107
|
+
let skipAll = false
|
|
108
|
+
// Group findings by secretKey (preserving insertion order)
|
|
109
|
+
const byKey = new Map()
|
|
110
|
+
for (const f of findings) {
|
|
111
|
+
if (!byKey.has(f.secretKey)) byKey.set(f.secretKey, [])
|
|
112
|
+
byKey.get(f.secretKey).push(f)
|
|
113
|
+
}
|
|
114
|
+
for (const [secretKey, keyFindings] of byKey) {
|
|
115
|
+
if (skipAll) {
|
|
116
|
+
skippedKeys++
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
console.log()
|
|
120
|
+
const scope = keyFindings[0].scope || 'global'
|
|
121
|
+
const scopeLabel = scope === 'global' ? 'global' : `project: ${secretsManager.projectName}`
|
|
122
|
+
if (keyFindings.length === 1) {
|
|
123
|
+
// Single finding — simple prompt
|
|
124
|
+
const f = keyFindings[0]
|
|
125
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(secretKey)} — ${chalk.white(`${f.file}:${f.line}`)}`)
|
|
126
|
+
console.log(` ${f.message}`)
|
|
127
|
+
console.log(` Value: ${chalk.dim(maskValue(f.value))}`)
|
|
128
|
+
const { action } = await inquirer.prompt([{
|
|
129
|
+
type: 'list',
|
|
130
|
+
name: 'action',
|
|
131
|
+
message: `Migrate ${chalk.cyan(secretKey)} to keychain and fix ${f.file}?`,
|
|
132
|
+
choices: [
|
|
133
|
+
{ name: 'Yes', value: 'yes' },
|
|
134
|
+
{ name: 'Skip', value: 'skip' },
|
|
135
|
+
{ name: 'Skip all remaining', value: 'skip_all' }
|
|
136
|
+
]
|
|
137
|
+
}])
|
|
138
|
+
if (action === 'skip_all') {
|
|
139
|
+
skipAll = true
|
|
140
|
+
skippedKeys++
|
|
141
|
+
console.log(` ${chalk.dim('⊘ skipped')}`)
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (action === 'skip') {
|
|
145
|
+
skippedKeys++
|
|
146
|
+
console.log(` ${chalk.dim('⊘ skipped')}`)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
await secretsManager.set(secretKey, f.value, { scope })
|
|
150
|
+
this.fixFinding(f)
|
|
151
|
+
console.log(` ${chalk.green('✓')} saved ${chalk.cyan(secretKey)} (${scopeLabel}) — ${f.file}:${f.line} updated`)
|
|
152
|
+
migratedKeys++
|
|
153
|
+
fixedFiles++
|
|
154
|
+
} else {
|
|
155
|
+
// Multiple findings — show all locations, offer bulk fix
|
|
156
|
+
console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(secretKey)} — found in ${chalk.bold(keyFindings.length)} files:`)
|
|
157
|
+
for (let i = 0; i < keyFindings.length; i++) {
|
|
158
|
+
const f = keyFindings[i]
|
|
159
|
+
console.log(` ${i + 1}. ${chalk.white(`${f.file}:${f.line}`)} — ${f.message}`)
|
|
160
|
+
}
|
|
161
|
+
console.log(` Value: ${chalk.dim(maskValue(keyFindings[0].value))}`)
|
|
162
|
+
const { action } = await inquirer.prompt([{
|
|
163
|
+
type: 'list',
|
|
164
|
+
name: 'action',
|
|
165
|
+
message: `Fix ${chalk.cyan(secretKey)}?`,
|
|
166
|
+
choices: [
|
|
167
|
+
{ name: `Fix all ${keyFindings.length} usages`, value: 'fix_all' },
|
|
168
|
+
{ name: 'Choose per file', value: 'per_file' },
|
|
169
|
+
{ name: 'Skip this key', value: 'skip' },
|
|
170
|
+
{ name: 'Skip all remaining', value: 'skip_all' }
|
|
171
|
+
]
|
|
172
|
+
}])
|
|
173
|
+
if (action === 'skip_all') {
|
|
174
|
+
skipAll = true
|
|
175
|
+
skippedKeys++
|
|
176
|
+
console.log(` ${chalk.dim('⊘ skipped')}`)
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if (action === 'skip') {
|
|
180
|
+
skippedKeys++
|
|
181
|
+
console.log(` ${chalk.dim('⊘ skipped')}`)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
if (action === 'fix_all') {
|
|
185
|
+
await secretsManager.set(secretKey, keyFindings[0].value, { scope })
|
|
186
|
+
console.log(` ${chalk.green('✓')} saved ${chalk.cyan(secretKey)} (${scopeLabel})`)
|
|
187
|
+
for (const f of keyFindings) {
|
|
188
|
+
this.fixFinding(f)
|
|
189
|
+
console.log(` ${chalk.green('✓')} ${f.file}:${f.line} updated`)
|
|
190
|
+
fixedFiles++
|
|
191
|
+
}
|
|
192
|
+
migratedKeys++
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
// per_file — prompt for each file individually
|
|
196
|
+
let savedToKeychain = false
|
|
197
|
+
let anyFixed = false
|
|
198
|
+
for (const f of keyFindings) {
|
|
199
|
+
const { fileAction } = await inquirer.prompt([{
|
|
200
|
+
type: 'list',
|
|
201
|
+
name: 'fileAction',
|
|
202
|
+
message: ` Fix ${chalk.white(`${f.file}:${f.line}`)}?`,
|
|
203
|
+
choices: [
|
|
204
|
+
{ name: 'Yes', value: 'yes' },
|
|
205
|
+
{ name: 'Skip', value: 'skip' }
|
|
206
|
+
]
|
|
207
|
+
}])
|
|
208
|
+
if (fileAction === 'skip') {
|
|
209
|
+
console.log(` ${chalk.dim('⊘ skipped')}`)
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
if (!savedToKeychain) {
|
|
213
|
+
await secretsManager.set(secretKey, f.value, { scope })
|
|
214
|
+
console.log(` ${chalk.green('✓')} saved ${chalk.cyan(secretKey)} (${scopeLabel})`)
|
|
215
|
+
savedToKeychain = true
|
|
216
|
+
}
|
|
217
|
+
this.fixFinding(f)
|
|
218
|
+
console.log(` ${chalk.green('✓')} ${f.file}:${f.line} updated`)
|
|
219
|
+
fixedFiles++
|
|
220
|
+
anyFixed = true
|
|
221
|
+
}
|
|
222
|
+
if (anyFixed) migratedKeys++
|
|
223
|
+
else skippedKeys++
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
console.log()
|
|
227
|
+
if (migratedKeys === 0 && skippedKeys === 0) {
|
|
228
|
+
console.log(` ${chalk.green('✓')} Nothing to migrate`)
|
|
229
|
+
} else {
|
|
230
|
+
const fileLabel = fixedFiles === 1 ? 'file' : 'files'
|
|
231
|
+
console.log(` ${chalk.green('✓')} migrated ${chalk.bold(migratedKeys)} secrets (${fixedFiles} ${fileLabel} fixed), skipped ${skippedKeys}`)
|
|
232
|
+
}
|
|
233
|
+
console.log()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_scanNpmrc (findings) {
|
|
237
|
+
const locations = [
|
|
238
|
+
path.join(this._projectDir, '.npmrc'),
|
|
239
|
+
path.join(os.homedir(), '.npmrc')
|
|
240
|
+
]
|
|
241
|
+
for (const filePath of locations) {
|
|
242
|
+
if (!fs.existsSync(filePath)) continue
|
|
243
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n')
|
|
244
|
+
for (let i = 0; i < lines.length; i++) {
|
|
245
|
+
const line = lines[i].trim()
|
|
246
|
+
const match = line.match(/_authToken=(.+)/)
|
|
247
|
+
if (!match) continue
|
|
248
|
+
const value = match[1].trim()
|
|
249
|
+
if (/^\$\{.+\}$/.test(value)) continue
|
|
250
|
+
const preview = value.substring(0, 8)
|
|
251
|
+
const isHomeNpmrc = filePath === path.join(os.homedir(), '.npmrc')
|
|
252
|
+
const displayPath = isHomeNpmrc
|
|
253
|
+
? '~/.npmrc'
|
|
254
|
+
: path.relative(this._projectDir, filePath)
|
|
255
|
+
findings.push({
|
|
256
|
+
file: displayPath,
|
|
257
|
+
line: i + 1,
|
|
258
|
+
type: 'npm_token',
|
|
259
|
+
message: `contains auth token (${preview}...)`,
|
|
260
|
+
fix: 'goki-secrets set NPM_TOKEN --global',
|
|
261
|
+
value,
|
|
262
|
+
secretKey: 'NPM_TOKEN',
|
|
263
|
+
scope: 'global'
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_scanEnvFiles (findings) {
|
|
270
|
+
for (const fileName of ENV_FILES) {
|
|
271
|
+
const filePath = path.join(this._projectDir, fileName)
|
|
272
|
+
if (!fs.existsSync(filePath)) continue
|
|
273
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n')
|
|
274
|
+
for (let i = 0; i < lines.length; i++) {
|
|
275
|
+
const line = lines[i].trim()
|
|
276
|
+
if (!line || line.startsWith('#')) continue
|
|
277
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/)
|
|
278
|
+
if (!match) continue
|
|
279
|
+
const key = match[1]
|
|
280
|
+
const value = match[2].trim().replace(/^["']|["']$/g, '')
|
|
281
|
+
if (!SENSITIVE_PATTERNS.test(key)) continue
|
|
282
|
+
if (isPlaceholder(value)) continue
|
|
283
|
+
findings.push({
|
|
284
|
+
file: fileName,
|
|
285
|
+
line: i + 1,
|
|
286
|
+
type: 'env_secret',
|
|
287
|
+
message: `${key} contains plaintext value`,
|
|
288
|
+
fix: `goki-secrets set ${key}`,
|
|
289
|
+
value,
|
|
290
|
+
secretKey: key,
|
|
291
|
+
scope: 'project'
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_scanConfigFiles (findings) {
|
|
298
|
+
for (const fileName of CONFIG_FILES) {
|
|
299
|
+
const filePath = path.join(this._projectDir, fileName)
|
|
300
|
+
if (!fs.existsSync(filePath)) continue
|
|
301
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n')
|
|
302
|
+
for (let i = 0; i < lines.length; i++) {
|
|
303
|
+
const line = lines[i].trim()
|
|
304
|
+
if (!line || line.startsWith('#') || line.startsWith('//')) continue
|
|
305
|
+
const match = line.match(/([A-Z_][A-Z0-9_]*)[\s]*[=:][\s]*(.+)/)
|
|
306
|
+
if (!match) continue
|
|
307
|
+
const key = match[1]
|
|
308
|
+
const value = match[2].trim().replace(/[,'"]/g, '').trim()
|
|
309
|
+
if (!SENSITIVE_PATTERNS.test(key)) continue
|
|
310
|
+
if (isPlaceholder(value)) continue
|
|
311
|
+
findings.push({
|
|
312
|
+
file: fileName,
|
|
313
|
+
line: i + 1,
|
|
314
|
+
type: 'config_secret',
|
|
315
|
+
message: `${key} in plaintext`,
|
|
316
|
+
fix: `goki-secrets set ${key}`,
|
|
317
|
+
value,
|
|
318
|
+
secretKey: key,
|
|
319
|
+
scope: 'project'
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_scanComposeFiles (findings) {
|
|
326
|
+
const composeGlobs = this._findComposeFiles()
|
|
327
|
+
for (const filePath of composeGlobs) {
|
|
328
|
+
if (!fs.existsSync(filePath)) continue
|
|
329
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n')
|
|
330
|
+
let inEnvironment = false
|
|
331
|
+
for (let i = 0; i < lines.length; i++) {
|
|
332
|
+
const line = lines[i]
|
|
333
|
+
const trimmed = line.trim()
|
|
334
|
+
if (/^environment\s*:/.test(trimmed)) {
|
|
335
|
+
inEnvironment = true
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
if (inEnvironment && trimmed && !trimmed.startsWith('-') && !trimmed.startsWith('#') && !trimmed.includes('=') && !trimmed.includes(':')) {
|
|
339
|
+
inEnvironment = false
|
|
340
|
+
}
|
|
341
|
+
if (inEnvironment && /^\S/.test(line) && !/^\s/.test(line)) {
|
|
342
|
+
inEnvironment = false
|
|
343
|
+
}
|
|
344
|
+
if (!inEnvironment) continue
|
|
345
|
+
const listMatch = trimmed.match(/^-\s*([A-Z_][A-Z0-9_]*)=(.+)$/)
|
|
346
|
+
const mapMatch = trimmed.match(/^([A-Z_][A-Z0-9_]*)\s*:\s*(.+)$/)
|
|
347
|
+
const kvMatch = listMatch || mapMatch
|
|
348
|
+
if (!kvMatch) continue
|
|
349
|
+
const key = kvMatch[1]
|
|
350
|
+
const value = kvMatch[2].trim().replace(/^["']|["']$/g, '')
|
|
351
|
+
if (!SENSITIVE_PATTERNS.test(key)) continue
|
|
352
|
+
if (isPlaceholder(value)) continue
|
|
353
|
+
findings.push({
|
|
354
|
+
file: path.relative(this._projectDir, filePath),
|
|
355
|
+
line: i + 1,
|
|
356
|
+
type: 'compose_secret',
|
|
357
|
+
message: `${key} hardcoded as "${value}"`,
|
|
358
|
+
fix: `goki-secrets set ${key} --global`,
|
|
359
|
+
value,
|
|
360
|
+
secretKey: key,
|
|
361
|
+
scope: 'global'
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_findComposeFiles () {
|
|
368
|
+
const files = []
|
|
369
|
+
for (const name of COMPOSE_FILES) {
|
|
370
|
+
files.push(path.join(this._projectDir, name))
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const entries = fs.readdirSync(this._projectDir)
|
|
374
|
+
for (const entry of entries) {
|
|
375
|
+
if (/^docker-compose\..+\.(yml|yaml)$/.test(entry) && !COMPOSE_FILES.includes(entry)) {
|
|
376
|
+
files.push(path.join(this._projectDir, entry))
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
// Directory not readable, skip
|
|
381
|
+
}
|
|
382
|
+
return files
|
|
383
|
+
}
|
|
384
|
+
}
|