Skip to content

Commit 2e3666e

Browse files
committed
🟢 fix feedback issues
1 parent 8370ba1 commit 2e3666e

File tree

6 files changed

+177
-110
lines changed

6 files changed

+177
-110
lines changed

.github/actions/spelling/expect.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ apparmor
1212
appslot
1313
arp
1414
artifactregistry
15+
asdk
1516
atlassian
1617
auditlog
1718
Auths
@@ -39,6 +40,7 @@ cdn
3940
certificatechains
4041
ciscocatalyst
4142
clcerts
43+
chatgpt
4244
chokepoint
4345
cloudflare
4446
claude
@@ -109,6 +111,7 @@ gistfile
109111
gpc
110112
gpo
111113
gpu
114+
gopls
112115
grafana
113116
groupname
114117
gvnic
@@ -207,6 +210,7 @@ oidc
207210
ondemand
208211
ontap
209212
opcplc
213+
openai
210214
opensearch
211215
openssl
212216
openvpn

providers/os/resources/claude_code.go

Lines changed: 74 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import (
1414
"sync"
1515
"time"
1616

17+
"github.com/spf13/afero"
1718
"go.mondoo.com/mql/v13/llx"
1819
"go.mondoo.com/mql/v13/providers-sdk/v1/plugin"
20+
"go.mondoo.com/mql/v13/providers/os/connection/shared"
1921
"go.mondoo.com/mql/v13/types"
2022
"sigs.k8s.io/yaml"
2123
)
@@ -34,16 +36,25 @@ func initClaudeCode(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[
3436
}
3537

3638
if _, ok := args["configPath"]; !ok {
37-
home, err := os.UserHomeDir()
39+
// Resolve the home directory from the target's user list, not the local host.
40+
home, err := targetHomeDir(runtime)
3841
if err != nil {
39-
return nil, nil, fmt.Errorf("cannot determine user home directory: %w", err)
42+
return nil, nil, err
4043
}
4144
args["configPath"] = llx.StringData(filepath.Join(home, defaultClaudeCodeConfigDir))
4245
}
4346

4447
return args, nil, nil
4548
}
4649

50+
// mqlClaudeCodeInternal caches the backup state for the lifetime of
51+
// this resource instance, avoiding the global map that leaked across assets.
52+
type mqlClaudeCodeInternal struct {
53+
backupOnce sync.Once
54+
backupState *claudeBackupState
55+
backupErr error
56+
}
57+
4758
func (r *mqlClaudeCode) id() (string, error) {
4859
return "claude.code/" + r.ConfigPath.Data, nil
4960
}
@@ -53,6 +64,12 @@ func (r *mqlClaudeCode) configDir() string {
5364
return r.ConfigPath.Data
5465
}
5566

67+
// afs returns an afero.Afero wrapping the connection's filesystem.
68+
func (r *mqlClaudeCode) afs() *afero.Afero {
69+
conn := r.MqlRuntime.Connection.(shared.Connection)
70+
return &afero.Afero{Fs: conn.FileSystem()}
71+
}
72+
5673
func (r *mqlClaudeCode) email() (string, error) {
5774
acct, err := r.loadOAuthAccount()
5875
if err != nil {
@@ -103,7 +120,7 @@ func (r *mqlClaudeCode) organizationId() (string, error) {
103120

104121
func (r *mqlClaudeCode) settings() (interface{}, error) {
105122
var settings map[string]interface{}
106-
err := claudeReadJSONFile(r.configDir(), "settings.json", &settings)
123+
err := readJSONFileAfero(r.afs(), r.configDir(), "settings.json", &settings)
107124
if err != nil {
108125
if os.IsNotExist(err) {
109126
return map[string]interface{}{}, nil
@@ -117,7 +134,7 @@ func (r *mqlClaudeCode) enabledPlugins() ([]interface{}, error) {
117134
var settings struct {
118135
EnabledPlugins map[string]bool `json:"enabledPlugins"`
119136
}
120-
err := claudeReadJSONFile(r.configDir(), "settings.json", &settings)
137+
err := readJSONFileAfero(r.afs(), r.configDir(), "settings.json", &settings)
121138
if err != nil {
122139
if os.IsNotExist(err) {
123140
return nil, nil
@@ -137,11 +154,13 @@ func (r *mqlClaudeCode) enabledPlugins() ([]interface{}, error) {
137154
}
138155

139156
func (r *mqlClaudeCode) plugins() ([]interface{}, error) {
157+
afs := r.afs()
158+
140159
var installedPlugins struct {
141160
Version int `json:"version"`
142161
Plugins map[string][]installedPluginEntry `json:"plugins"`
143162
}
144-
err := claudeReadJSONFile(r.configDir(), "plugins/installed_plugins.json", &installedPlugins)
163+
err := readJSONFileAfero(afs, r.configDir(), "plugins/installed_plugins.json", &installedPlugins)
145164
if err != nil {
146165
if os.IsNotExist(err) {
147166
return nil, nil
@@ -152,7 +171,7 @@ func (r *mqlClaudeCode) plugins() ([]interface{}, error) {
152171
var settings struct {
153172
EnabledPlugins map[string]bool `json:"enabledPlugins"`
154173
}
155-
_ = claudeReadJSONFile(r.configDir(), "settings.json", &settings)
174+
_ = readJSONFileAfero(afs, r.configDir(), "settings.json", &settings)
156175

157176
var result []interface{}
158177
for name, entries := range installedPlugins.Plugins {
@@ -184,9 +203,10 @@ func (r *mqlClaudeCode) plugins() ([]interface{}, error) {
184203
}
185204

186205
func (r *mqlClaudeCode) skills() ([]interface{}, error) {
206+
afs := r.afs()
187207
skillsDir := filepath.Join(r.configDir(), "skills")
188208

189-
entries, err := os.ReadDir(skillsDir)
209+
entries, err := afs.ReadDir(skillsDir)
190210
if err != nil {
191211
if os.IsNotExist(err) {
192212
return nil, nil
@@ -201,7 +221,7 @@ func (r *mqlClaudeCode) skills() ([]interface{}, error) {
201221
}
202222

203223
skillPath := filepath.Join(skillsDir, entry.Name(), "SKILL.md")
204-
data, err := os.ReadFile(skillPath)
224+
data, err := afs.ReadFile(skillPath)
205225
if err != nil {
206226
continue
207227
}
@@ -231,6 +251,7 @@ func (r *mqlClaudeCode) skills() ([]interface{}, error) {
231251
}
232252

233253
func (r *mqlClaudeCode) projects() ([]interface{}, error) {
254+
afs := r.afs()
234255
state, err := r.loadBackupState()
235256
if err != nil {
236257
return nil, err
@@ -240,7 +261,7 @@ func (r *mqlClaudeCode) projects() ([]interface{}, error) {
240261
var result []interface{}
241262
for projectPath, dirName := range state.projectDirMap() {
242263
memoryDir := filepath.Join(projectsDir, dirName, "memory")
243-
hasMemory := dirHasFiles(memoryDir)
264+
hasMemory := dirHasFilesAfero(afs, memoryDir)
244265

245266
res, err := NewResource(r.MqlRuntime, "claude.code.project", map[string]*llx.RawData{
246267
"__id": llx.StringData("claude.code.project/" + projectPath),
@@ -259,7 +280,7 @@ func (r *mqlClaudeCode) mcpServers() ([]interface{}, error) {
259280
var cache map[string]struct {
260281
Timestamp int64 `json:"timestamp"`
261282
}
262-
err := claudeReadJSONFile(r.configDir(), "mcp-needs-auth-cache.json", &cache)
283+
err := readJSONFileAfero(r.afs(), r.configDir(), "mcp-needs-auth-cache.json", &cache)
263284
if err != nil {
264285
if os.IsNotExist(err) {
265286
return nil, nil
@@ -274,6 +295,8 @@ func (r *mqlClaudeCode) mcpServers() ([]interface{}, error) {
274295
lastChecked = time.UnixMilli(entry.Timestamp).UTC().Format(time.RFC3339)
275296
}
276297

298+
// Presence in mcp-needs-auth-cache.json means the server requires
299+
// authentication; servers that don't need auth are not listed.
277300
res, err := NewResource(r.MqlRuntime, "claude.code.mcpServer", map[string]*llx.RawData{
278301
"__id": llx.StringData("claude.code.mcpServer/" + name),
279302
"name": llx.StringData(name),
@@ -341,45 +364,25 @@ func pathToProjectDir(path string) string {
341364
return "-" + s
342365
}
343366

344-
// backupStateOnce caches the backup state per resource instance,
345-
// loaded at most once via sync.Once.
346-
type backupStateOnce struct {
347-
once sync.Once
348-
state *claudeBackupState
349-
err error
350-
}
351-
352-
var (
353-
backupStateInstances = make(map[string]*backupStateOnce)
354-
backupStateInstancesMu sync.Mutex
355-
)
356-
357367
func (r *mqlClaudeCode) loadBackupState() (*claudeBackupState, error) {
358-
dir := r.configDir()
359-
360-
backupStateInstancesMu.Lock()
361-
bso, ok := backupStateInstances[dir]
362-
if !ok {
363-
bso = &backupStateOnce{}
364-
backupStateInstances[dir] = bso
365-
}
366-
backupStateInstancesMu.Unlock()
368+
r.backupOnce.Do(func() {
369+
afs := r.afs()
370+
dir := r.configDir()
367371

368-
bso.once.Do(func() {
369-
backupFile, err := findLatestBackup(dir)
372+
backupFile, err := findLatestBackupAfero(afs, dir)
370373
if err != nil {
371-
bso.err = err
374+
r.backupErr = err
372375
return
373376
}
374377
var state claudeBackupState
375-
if err := claudeReadJSONFile(dir, filepath.Join("backups", backupFile), &state); err != nil {
376-
bso.err = err
378+
if err := readJSONFileAfero(afs, dir, filepath.Join("backups", backupFile), &state); err != nil {
379+
r.backupErr = err
377380
return
378381
}
379-
bso.state = &state
382+
r.backupState = &state
380383
})
381384

382-
return bso.state, bso.err
385+
return r.backupState, r.backupErr
383386
}
384387

385388
func (r *mqlClaudeCode) loadOAuthAccount() (*oauthAccount, error) {
@@ -393,9 +396,9 @@ func (r *mqlClaudeCode) loadOAuthAccount() (*oauthAccount, error) {
393396
return state.OAuthAccount, nil
394397
}
395398

396-
func findLatestBackup(configDir string) (string, error) {
399+
func findLatestBackupAfero(afs *afero.Afero, configDir string) (string, error) {
397400
backupsDir := filepath.Join(configDir, "backups")
398-
entries, err := os.ReadDir(backupsDir)
401+
entries, err := afs.ReadDir(backupsDir)
399402
if err != nil {
400403
return "", fmt.Errorf("cannot read backups directory: %w", err)
401404
}
@@ -424,9 +427,10 @@ func findLatestBackup(configDir string) (string, error) {
424427
return latestBackup, nil
425428
}
426429

427-
// claudeReadJSONFile reads and unmarshals a JSON file relative to a base directory.
428-
func claudeReadJSONFile(baseDir string, relPath string, v interface{}) error {
429-
data, err := os.ReadFile(filepath.Join(baseDir, relPath))
430+
// readJSONFileAfero reads and unmarshals a JSON file relative to a base directory
431+
// using the provided afero filesystem (which may be remote via SSH, container, etc.).
432+
func readJSONFileAfero(afs *afero.Afero, baseDir string, relPath string, v interface{}) error {
433+
data, err := afs.ReadFile(filepath.Join(baseDir, relPath))
430434
if err != nil {
431435
return err
432436
}
@@ -482,8 +486,8 @@ func parseSkillMd(name, sourcePath, content string) skillInfo {
482486
return info
483487
}
484488

485-
func dirHasFiles(dir string) bool {
486-
entries, err := os.ReadDir(dir)
489+
func dirHasFilesAfero(afs *afero.Afero, dir string) bool {
490+
entries, err := afs.ReadDir(dir)
487491
if err != nil {
488492
return false
489493
}
@@ -495,6 +499,30 @@ func dirHasFiles(dir string) bool {
495499
return false
496500
}
497501

502+
// targetHomeDir resolves the first non-system user's home directory on the target
503+
// via the users resource, so it works for remote SSH/container connections.
504+
func targetHomeDir(runtime *plugin.Runtime) (string, error) {
505+
usersResource, err := CreateResource(runtime, "users", map[string]*llx.RawData{})
506+
if err != nil {
507+
return "", fmt.Errorf("cannot list users on target: %w", err)
508+
}
509+
510+
userList := usersResource.(*mqlUsers).GetList()
511+
if userList.Error != nil {
512+
return "", fmt.Errorf("cannot list users on target: %w", userList.Error)
513+
}
514+
515+
for _, u := range userList.Data {
516+
user := u.(*mqlUser)
517+
home := user.GetHome().Data
518+
if home != "" && !invalidHomeDirs[home] {
519+
return home, nil
520+
}
521+
}
522+
523+
return "", fmt.Errorf("no valid user home directory found on target")
524+
}
525+
498526
// Stub ID methods for child resources (they use __id set during creation)
499527

500528
func (r *mqlClaudeCodePlugin) id() (string, error) {

0 commit comments

Comments
 (0)