@codyswann/lisa 2.333.3 → 2.334.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/all/copy-overwrite/scripts/lisa-hooks/sonar-secrets.sh +139 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +13 -3
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +113 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-sonar/SKILL.md +45 -0
- package/plugins/lisa/hooks/sonar-secrets.sh +139 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +113 -3
- package/plugins/lisa/skills/lisa-setup-sonar/SKILL.md +45 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +113 -3
- package/plugins/lisa-agy/skills/lisa-setup-sonar/SKILL.md +45 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/sonar-secrets.sh +139 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +113 -3
- package/plugins/lisa-copilot/skills/lisa-setup-sonar/SKILL.md +45 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/sonar-secrets.sh +139 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +113 -3
- package/plugins/lisa-cursor/skills/lisa-setup-sonar/SKILL.md +45 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/sonar-secrets.sh +139 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +113 -3
- package/plugins/src/base/skills/lisa-setup-sonar/SKILL.md +45 -0
- package/scripts/build-plugins.sh +14 -0
|
@@ -191,6 +191,38 @@ export function installProfileSourcing(valuesFile, options = {}) {
|
|
|
191
191
|
* @param {object} [options] Home directory and file seams, for tests.
|
|
192
192
|
* @returns {string[]} The profile names written.
|
|
193
193
|
*/
|
|
194
|
+
/**
|
|
195
|
+
* Profile names already defined OUTSIDE this module's managed block.
|
|
196
|
+
*
|
|
197
|
+
* Only what lies outside the block counts: our own previous output is meant to
|
|
198
|
+
* be replaced, and treating it as a collision would make the second run fail.
|
|
199
|
+
* @param {string} dir The `.aws` directory.
|
|
200
|
+
* @param {string[]} names Profile names about to be written.
|
|
201
|
+
* @param {object} io `exists` and `read` seams.
|
|
202
|
+
* @returns {string[]} Colliding names, in the order given.
|
|
203
|
+
*/
|
|
204
|
+
export function collidingProfiles(dir, names, io = {}) {
|
|
205
|
+
const { exists = existsSync, read = readFileSync } = io;
|
|
206
|
+
const file = join(dir, "config");
|
|
207
|
+
if (!exists(file)) return [];
|
|
208
|
+
|
|
209
|
+
const text = String(read(file, "utf8"));
|
|
210
|
+
const start = text.indexOf(MANAGED_MARKER);
|
|
211
|
+
const endAt = start === -1 ? -1 : text.indexOf(MANAGED_END, start);
|
|
212
|
+
const outside =
|
|
213
|
+
start === -1
|
|
214
|
+
? text
|
|
215
|
+
: text.slice(0, start) +
|
|
216
|
+
(endAt === -1 ? "" : text.slice(endAt + MANAGED_END.length));
|
|
217
|
+
|
|
218
|
+
return names.filter(name =>
|
|
219
|
+
new RegExp(
|
|
220
|
+
`^\\s*\\[profile\\s+${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\]`,
|
|
221
|
+
"m"
|
|
222
|
+
).test(outside)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
194
226
|
export function installAwsProfiles(bundle, options = {}) {
|
|
195
227
|
const {
|
|
196
228
|
home = process.env.HOME || homedir(),
|
|
@@ -205,6 +237,33 @@ export function installAwsProfiles(bundle, options = {}) {
|
|
|
205
237
|
if (!rendered) return [];
|
|
206
238
|
|
|
207
239
|
const dir = join(home, ".aws");
|
|
240
|
+
|
|
241
|
+
// Refuse to write a profile name the operator already uses outside our block.
|
|
242
|
+
//
|
|
243
|
+
// AWS does not error on a duplicate `[profile x]` — it resolves one and
|
|
244
|
+
// ignores the other. So writing `tunnl-dev` next to an operator's existing SSO
|
|
245
|
+
// `tunnl-dev` would silently run some calls as the wrong identity, which is
|
|
246
|
+
// worse than either winning outright. Merging protects their sections from
|
|
247
|
+
// being deleted; this protects them from being shadowed.
|
|
248
|
+
//
|
|
249
|
+
// Deliberately not resolved by renaming theirs: this module writes its own
|
|
250
|
+
// block and nothing else. The operator renames (conventionally to `-sso`) and
|
|
251
|
+
// re-runs.
|
|
252
|
+
const collisions = collidingProfiles(dir, rendered.profiles, {
|
|
253
|
+
exists,
|
|
254
|
+
read,
|
|
255
|
+
});
|
|
256
|
+
if (collisions.length > 0) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`~/.aws/config already defines ${collisions.map(n => `"${n}"`).join(", ")} ` +
|
|
259
|
+
`outside the lisa-managed block.\n` +
|
|
260
|
+
`Writing them would create duplicate sections, and AWS resolves only ` +
|
|
261
|
+
`one — some calls would silently use the wrong identity.\n` +
|
|
262
|
+
`Rename the existing entries (for example to "${collisions[0]}-sso") ` +
|
|
263
|
+
`and run this again.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
208
267
|
mkdir(dir, { recursive: true, mode: 0o700 });
|
|
209
268
|
chmod(dir, 0o700);
|
|
210
269
|
|
|
@@ -297,6 +356,47 @@ export function materialize(cfg = readConfig()) {
|
|
|
297
356
|
|
|
298
357
|
function main() {
|
|
299
358
|
const cfg = readConfig();
|
|
359
|
+
|
|
360
|
+
// `--aws-profiles-only` writes ~/.aws and NOTHING else, on any surface.
|
|
361
|
+
//
|
|
362
|
+
// A laptop refuses to materialize secrets to disk, deliberately: read-through
|
|
363
|
+
// to the provider adds no drift and leaves no copy. That rule is about the
|
|
364
|
+
// thirteen values in secrets.env, and it stays exactly as it was here.
|
|
365
|
+
//
|
|
366
|
+
// The AWS profiles are a different bargain, requested explicitly. Agents
|
|
367
|
+
// working on a developer's machine should act as RemoteAgent rather than
|
|
368
|
+
// borrowing the human's SSO identity — separate attribution in CloudTrail,
|
|
369
|
+
// the role's blast radius instead of a person's, and the same `agent-*`
|
|
370
|
+
// profile names as a container so a script does not need two vocabularies.
|
|
371
|
+
//
|
|
372
|
+
// The cost is real and belongs in the open: `source_profile` needs a
|
|
373
|
+
// long-lived key pair, so this writes one to a machine that is not
|
|
374
|
+
// disposable, which is the thing the local surface otherwise prevents. It is
|
|
375
|
+
// therefore opt-in per run, never automatic, and never part of a normal
|
|
376
|
+
// materialize.
|
|
377
|
+
if (process.argv.includes("--aws-profiles-only")) {
|
|
378
|
+
const selected = fetchAll(cfg);
|
|
379
|
+
const bundle = parseBootstrap(selected.get(BOOTSTRAP_KEY)?.value);
|
|
380
|
+
const written = installAwsProfiles(bundle);
|
|
381
|
+
|
|
382
|
+
if (written.length === 0) {
|
|
383
|
+
console.log(
|
|
384
|
+
`no AWS profiles written — ${BOOTSTRAP_KEY} is absent, unparseable, ` +
|
|
385
|
+
`or declares no profile carrying a roleArn.`
|
|
386
|
+
);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
console.log(
|
|
390
|
+
`wrote ${written.length} AWS profile(s): ${written.join(", ")}`
|
|
391
|
+
);
|
|
392
|
+
console.log(
|
|
393
|
+
` Sourced from a long-lived key pair in ~/.aws/credentials. Existing\n` +
|
|
394
|
+
` sections were preserved — only the lisa-managed block was replaced.\n` +
|
|
395
|
+
` Use them explicitly: aws --profile ${written[0]} ...`
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
300
400
|
if (process.argv.includes("--dry-run")) {
|
|
301
401
|
const selected = fetchAll(cfg);
|
|
302
402
|
// Derive here too, or the preview lies in the one place it matters most:
|
|
@@ -314,10 +414,20 @@ function main() {
|
|
|
314
414
|
const { count, derived, dir } = materialize(cfg);
|
|
315
415
|
console.log(`materialized ${count} secret(s) and their notes into ${dir}`);
|
|
316
416
|
if (derived > 0) {
|
|
317
|
-
//
|
|
417
|
+
// Do not claim an override this does not perform.
|
|
418
|
+
//
|
|
419
|
+
// This line used to end "overriding any ambient value". That was true when
|
|
420
|
+
// the key pair was exported and stopped being true when the pair moved into
|
|
421
|
+
// ~/.aws and AWS_PROFILE took its place — AWS_PROFILE loses to ambient
|
|
422
|
+
// environment credentials, it does not beat them. The stale sentence sent
|
|
423
|
+
// two separate investigations after a credential problem that did not
|
|
424
|
+
// exist, while the real fault was that nothing sourced the file. A message
|
|
425
|
+
// that overclaims costs more than no message.
|
|
318
426
|
console.log(
|
|
319
|
-
` ${derived} AWS variable(s) derived from the bootstrap bundle
|
|
320
|
-
`
|
|
427
|
+
` ${derived} AWS variable(s) derived from the bootstrap bundle. ` +
|
|
428
|
+
`These take\n effect only in a shell that sourced the materialized ` +
|
|
429
|
+
`env file; ambient\n credentials outrank AWS_PROFILE, so prefer an ` +
|
|
430
|
+
`explicit --profile.`
|
|
321
431
|
);
|
|
322
432
|
}
|
|
323
433
|
}
|
|
@@ -80,6 +80,51 @@ MCP config so it launches `sonar run mcp`, authenticated from the same env token
|
|
|
80
80
|
|
|
81
81
|
Skip any agent whose CLI is not installed; never wire Kiro or Gemini.
|
|
82
82
|
|
|
83
|
+
## 4a. Patch the generated shims — required, not optional
|
|
84
|
+
|
|
85
|
+
Each integrate writes a five-line shim that pipes the hook payload to `sonar hook
|
|
86
|
+
<event>` and passes the verdict through unchanged. That makes an
|
|
87
|
+
**unauthenticated CLI refuse every prompt and every file read**, with a reason
|
|
88
|
+
string that reads exactly like a real finding:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
SonarQube secret scanning is inactive: not authenticated. Run 'sonar auth login'.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The operator most likely to hit it is the one who just installed the CLI and has
|
|
95
|
+
not logged in — so their very first prompt is rejected, before the session can
|
|
96
|
+
say anything useful. Repoint every generated shim at Lisa's wrapper, which keeps
|
|
97
|
+
real findings blocking, resolves `SONARQUBE_CLI_TOKEN` through
|
|
98
|
+
`lisa-secrets-access` when the CLI reports it has none, and degrades the
|
|
99
|
+
still-unauthenticated case to a warning:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
if ! command -v sonar &> /dev/null; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
here="$(cd -- "$(dirname -- "$0")" && pwd)"
|
|
106
|
+
exec bash "${here}/<relative-path>/scripts/lisa-hooks/sonar-secrets.sh" <vendor-event>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
One wrapper serves every surface — it takes the vendor event name as its only
|
|
110
|
+
argument. Known shim locations and their events:
|
|
111
|
+
|
|
112
|
+
| Agent | Shim | Event |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| Claude | `.claude/hooks/sonar-secrets/build-scripts/prompt-secrets.sh` | `claude-prompt-submit` |
|
|
115
|
+
| Claude | `.claude/hooks/sonar-secrets/build-scripts/pretool-secrets.sh` | `claude-pre-tool-use` |
|
|
116
|
+
| Codex | `.codex/hooks/sonar-secrets/build-scripts/prompt-secrets.sh` | `codex-prompt-submit` |
|
|
117
|
+
| Copilot | `.github/hooks/sonar-secrets/build-scripts/pretool-secrets.sh` | `copilot-pre-tool-use` |
|
|
118
|
+
| Antigravity | `.agents/sonar/hooks/pretool-secrets.sh` | `antigravity-pre-tool-use` |
|
|
119
|
+
|
|
120
|
+
Compute the relative path from each shim's own directory rather than assuming a
|
|
121
|
+
depth — Antigravity's is three levels up, the rest are four.
|
|
122
|
+
|
|
123
|
+
**Re-running an integrate overwrites the shim with the vendor's original**, so
|
|
124
|
+
re-apply this patch whenever you re-run step 4, and say so in the summary. Leave
|
|
125
|
+
a comment at the top of each patched shim naming the wrapper, so the next reader
|
|
126
|
+
does not mistake the delegation for drift.
|
|
127
|
+
|
|
83
128
|
## 5. Write non-secret policy config
|
|
84
129
|
|
|
85
130
|
Merge only non-secret identifiers into committed `.lisa.config.json`, preserving
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Shared wrapper around `sonar hook <event>` for every agent surface.
|
|
4
|
+
#
|
|
5
|
+
# `sonar integrate <agent>` generates a five-line shim per surface: if `sonar` is
|
|
6
|
+
# on PATH, pipe the hook payload to it and let whatever comes back stand. That is
|
|
7
|
+
# correct for the thing the scanner exists to do — refusing a prompt or a file
|
|
8
|
+
# read that genuinely contains a credential — and wrong for the thing it also
|
|
9
|
+
# does, which is refusing *everything* when the CLI has no token yet:
|
|
10
|
+
#
|
|
11
|
+
# {"decision":"block","reason":"SonarQube secret scanning is inactive:
|
|
12
|
+
# not authenticated. Run 'sonar auth login'."}
|
|
13
|
+
#
|
|
14
|
+
# At the shim level that reply is indistinguishable from a real finding, so a
|
|
15
|
+
# workstation that had simply never run the browser login could not submit a
|
|
16
|
+
# prompt at all. Two things are wrong with that, and this wrapper fixes both.
|
|
17
|
+
#
|
|
18
|
+
# 1. The credential is usually already provisioned. A project that declares
|
|
19
|
+
# SONARQUBE_CLI_TOKEN under `secrets.require` has it in its configured
|
|
20
|
+
# provider — and the CLI reads only the environment and the OS keychain, so on
|
|
21
|
+
# a local surface (which deliberately materializes nothing to disk) the value
|
|
22
|
+
# is present and unreachable at the same time. We resolve it through
|
|
23
|
+
# lisa-secrets-access, the one sanctioned reader, and only when the CLI says
|
|
24
|
+
# it needs it.
|
|
25
|
+
#
|
|
26
|
+
# 2. An unauthenticated scanner is a degraded gate, not a breach. This hook is
|
|
27
|
+
# one of several overlapping layers: secret scanning at commit time and in CI
|
|
28
|
+
# does not depend on it. Blocking every prompt to protect a redundant layer
|
|
29
|
+
# trades a certain outage for a hypothetical leak — and the outage lands on
|
|
30
|
+
# exactly the person least able to diagnose it, since the block fires before
|
|
31
|
+
# their first prompt is ever seen.
|
|
32
|
+
#
|
|
33
|
+
# A real finding still blocks. Only the "inactive" reply is downgraded, and the
|
|
34
|
+
# distinction is drawn on the reason text the vendor emits, so a detection that
|
|
35
|
+
# arrives while unauthenticated is impossible by construction — in that state the
|
|
36
|
+
# scanner does not scan at all.
|
|
37
|
+
#
|
|
38
|
+
# Usage: sonar-secrets.sh <vendor-event-name> # hook payload on stdin
|
|
39
|
+
set -uo pipefail
|
|
40
|
+
|
|
41
|
+
event="${1:-}"
|
|
42
|
+
[ -n "$event" ] || exit 0
|
|
43
|
+
|
|
44
|
+
# Nothing installed, nothing to enforce. Matches the vendor shim's own guard.
|
|
45
|
+
command -v sonar >/dev/null 2>&1 || exit 0
|
|
46
|
+
|
|
47
|
+
# An explicit off switch, for bisecting a session that is misbehaving. There is
|
|
48
|
+
# deliberately no way to turn the *blocking* back on from the environment: the
|
|
49
|
+
# decision that a missing token must not stop work belongs in this file, where it
|
|
50
|
+
# is reviewable, not in whatever shell happened to launch the agent.
|
|
51
|
+
[ "${LISA_SONAR_HOOK:-on}" = "off" ] && exit 0
|
|
52
|
+
|
|
53
|
+
payload="$(cat)"
|
|
54
|
+
|
|
55
|
+
# The vendor signals its verdict as JSON on stdout and always exits 0, so the
|
|
56
|
+
# exit code carries no information and the reason text is the only channel.
|
|
57
|
+
inactive_marker='secret scanning is inactive'
|
|
58
|
+
|
|
59
|
+
run_scanner() {
|
|
60
|
+
printf '%s' "$payload" | sonar hook "$event" 2>/dev/null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
is_inactive() {
|
|
64
|
+
case "$1" in
|
|
65
|
+
*"$inactive_marker"*) return 0 ;;
|
|
66
|
+
*) return 1 ;;
|
|
67
|
+
esac
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Run the resolver with a ceiling, because the provider call crosses the network
|
|
71
|
+
# and this sits in front of every prompt and every file read. `timeout` is not
|
|
72
|
+
# present on a stock macOS, so this is the portable equivalent: start it, poll,
|
|
73
|
+
# give up. Giving up is safe — the caller treats an empty value as "the provider
|
|
74
|
+
# had nothing", which lands on the warn path rather than a block.
|
|
75
|
+
resolve_secret() {
|
|
76
|
+
local resolver="$1" name="$2" out pid waited=0
|
|
77
|
+
out="$(mktemp)" || return 0
|
|
78
|
+
node "$resolver" get "$name" >"$out" 2>/dev/null &
|
|
79
|
+
pid=$!
|
|
80
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
81
|
+
[ "$waited" -ge 10 ] && { kill -9 "$pid" 2>/dev/null; break; }
|
|
82
|
+
sleep 1
|
|
83
|
+
waited=$((waited + 1))
|
|
84
|
+
done
|
|
85
|
+
wait "$pid" 2>/dev/null
|
|
86
|
+
cat "$out"
|
|
87
|
+
rm -f "$out"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
out="$(run_scanner)"
|
|
91
|
+
|
|
92
|
+
if is_inactive "$out"; then
|
|
93
|
+
# Where lisa-secrets-access lives depends on how the project receives Lisa.
|
|
94
|
+
# Claude and Codex get it as an installed plugin, which is not part of a clone;
|
|
95
|
+
# the agent skill directories and node_modules are what a fresh checkout
|
|
96
|
+
# actually has. Same search order as the remote-env setup script, including the
|
|
97
|
+
# `plugins/` rung that exists only in the Lisa monorepo itself.
|
|
98
|
+
repo_root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
99
|
+
resolver=""
|
|
100
|
+
for candidate in \
|
|
101
|
+
"$repo_root/.claude/skills/lisa-secrets-access/scripts/resolve-secret.mjs" \
|
|
102
|
+
"$repo_root/.agents/skills/lisa-secrets-access/scripts/resolve-secret.mjs" \
|
|
103
|
+
"$repo_root/.codex/skills/lisa-secrets-access/scripts/resolve-secret.mjs" \
|
|
104
|
+
"$repo_root/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs" \
|
|
105
|
+
"$repo_root/node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs"; do
|
|
106
|
+
if [ -f "$candidate" ]; then
|
|
107
|
+
resolver="$candidate"
|
|
108
|
+
break
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
if [ -n "$resolver" ] && command -v node >/dev/null 2>&1; then
|
|
113
|
+
token="$(resolve_secret "$resolver" SONARQUBE_CLI_TOKEN)"
|
|
114
|
+
if [ -n "$token" ]; then
|
|
115
|
+
# Exported into this process only. Writing it anywhere durable would create
|
|
116
|
+
# a second live copy of a credential whose single store is the provider —
|
|
117
|
+
# see the one-store rule in lisa-secrets-access.
|
|
118
|
+
export SONARQUBE_CLI_TOKEN="$token"
|
|
119
|
+
org="$(resolve_secret "$resolver" SONARQUBE_CLI_ORG)"
|
|
120
|
+
[ -n "$org" ] && export SONARQUBE_CLI_ORG="$org"
|
|
121
|
+
out="$(run_scanner)"
|
|
122
|
+
fi
|
|
123
|
+
unset token org
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
if is_inactive "$out"; then
|
|
128
|
+
# stderr on a zero exit is advisory in every agent's hook protocol: the user
|
|
129
|
+
# sees it, the operation proceeds.
|
|
130
|
+
echo "sonar: local secret scanning is inactive (no token in the environment," >&2
|
|
131
|
+
echo " the OS keychain, or the configured secrets provider). Continuing —" >&2
|
|
132
|
+
echo " commit-time and CI secret scanning still gate this repository." >&2
|
|
133
|
+
echo " To restore it: 'sonar auth login', or store SONARQUBE_CLI_TOKEN in" >&2
|
|
134
|
+
echo " the project's secrets provider." >&2
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
[ -n "$out" ] && printf '%s' "$out"
|
|
139
|
+
exit 0
|
|
@@ -191,6 +191,38 @@ export function installProfileSourcing(valuesFile, options = {}) {
|
|
|
191
191
|
* @param {object} [options] Home directory and file seams, for tests.
|
|
192
192
|
* @returns {string[]} The profile names written.
|
|
193
193
|
*/
|
|
194
|
+
/**
|
|
195
|
+
* Profile names already defined OUTSIDE this module's managed block.
|
|
196
|
+
*
|
|
197
|
+
* Only what lies outside the block counts: our own previous output is meant to
|
|
198
|
+
* be replaced, and treating it as a collision would make the second run fail.
|
|
199
|
+
* @param {string} dir The `.aws` directory.
|
|
200
|
+
* @param {string[]} names Profile names about to be written.
|
|
201
|
+
* @param {object} io `exists` and `read` seams.
|
|
202
|
+
* @returns {string[]} Colliding names, in the order given.
|
|
203
|
+
*/
|
|
204
|
+
export function collidingProfiles(dir, names, io = {}) {
|
|
205
|
+
const { exists = existsSync, read = readFileSync } = io;
|
|
206
|
+
const file = join(dir, "config");
|
|
207
|
+
if (!exists(file)) return [];
|
|
208
|
+
|
|
209
|
+
const text = String(read(file, "utf8"));
|
|
210
|
+
const start = text.indexOf(MANAGED_MARKER);
|
|
211
|
+
const endAt = start === -1 ? -1 : text.indexOf(MANAGED_END, start);
|
|
212
|
+
const outside =
|
|
213
|
+
start === -1
|
|
214
|
+
? text
|
|
215
|
+
: text.slice(0, start) +
|
|
216
|
+
(endAt === -1 ? "" : text.slice(endAt + MANAGED_END.length));
|
|
217
|
+
|
|
218
|
+
return names.filter(name =>
|
|
219
|
+
new RegExp(
|
|
220
|
+
`^\\s*\\[profile\\s+${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\]`,
|
|
221
|
+
"m"
|
|
222
|
+
).test(outside)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
194
226
|
export function installAwsProfiles(bundle, options = {}) {
|
|
195
227
|
const {
|
|
196
228
|
home = process.env.HOME || homedir(),
|
|
@@ -205,6 +237,33 @@ export function installAwsProfiles(bundle, options = {}) {
|
|
|
205
237
|
if (!rendered) return [];
|
|
206
238
|
|
|
207
239
|
const dir = join(home, ".aws");
|
|
240
|
+
|
|
241
|
+
// Refuse to write a profile name the operator already uses outside our block.
|
|
242
|
+
//
|
|
243
|
+
// AWS does not error on a duplicate `[profile x]` — it resolves one and
|
|
244
|
+
// ignores the other. So writing `tunnl-dev` next to an operator's existing SSO
|
|
245
|
+
// `tunnl-dev` would silently run some calls as the wrong identity, which is
|
|
246
|
+
// worse than either winning outright. Merging protects their sections from
|
|
247
|
+
// being deleted; this protects them from being shadowed.
|
|
248
|
+
//
|
|
249
|
+
// Deliberately not resolved by renaming theirs: this module writes its own
|
|
250
|
+
// block and nothing else. The operator renames (conventionally to `-sso`) and
|
|
251
|
+
// re-runs.
|
|
252
|
+
const collisions = collidingProfiles(dir, rendered.profiles, {
|
|
253
|
+
exists,
|
|
254
|
+
read,
|
|
255
|
+
});
|
|
256
|
+
if (collisions.length > 0) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`~/.aws/config already defines ${collisions.map(n => `"${n}"`).join(", ")} ` +
|
|
259
|
+
`outside the lisa-managed block.\n` +
|
|
260
|
+
`Writing them would create duplicate sections, and AWS resolves only ` +
|
|
261
|
+
`one — some calls would silently use the wrong identity.\n` +
|
|
262
|
+
`Rename the existing entries (for example to "${collisions[0]}-sso") ` +
|
|
263
|
+
`and run this again.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
208
267
|
mkdir(dir, { recursive: true, mode: 0o700 });
|
|
209
268
|
chmod(dir, 0o700);
|
|
210
269
|
|
|
@@ -297,6 +356,47 @@ export function materialize(cfg = readConfig()) {
|
|
|
297
356
|
|
|
298
357
|
function main() {
|
|
299
358
|
const cfg = readConfig();
|
|
359
|
+
|
|
360
|
+
// `--aws-profiles-only` writes ~/.aws and NOTHING else, on any surface.
|
|
361
|
+
//
|
|
362
|
+
// A laptop refuses to materialize secrets to disk, deliberately: read-through
|
|
363
|
+
// to the provider adds no drift and leaves no copy. That rule is about the
|
|
364
|
+
// thirteen values in secrets.env, and it stays exactly as it was here.
|
|
365
|
+
//
|
|
366
|
+
// The AWS profiles are a different bargain, requested explicitly. Agents
|
|
367
|
+
// working on a developer's machine should act as RemoteAgent rather than
|
|
368
|
+
// borrowing the human's SSO identity — separate attribution in CloudTrail,
|
|
369
|
+
// the role's blast radius instead of a person's, and the same `agent-*`
|
|
370
|
+
// profile names as a container so a script does not need two vocabularies.
|
|
371
|
+
//
|
|
372
|
+
// The cost is real and belongs in the open: `source_profile` needs a
|
|
373
|
+
// long-lived key pair, so this writes one to a machine that is not
|
|
374
|
+
// disposable, which is the thing the local surface otherwise prevents. It is
|
|
375
|
+
// therefore opt-in per run, never automatic, and never part of a normal
|
|
376
|
+
// materialize.
|
|
377
|
+
if (process.argv.includes("--aws-profiles-only")) {
|
|
378
|
+
const selected = fetchAll(cfg);
|
|
379
|
+
const bundle = parseBootstrap(selected.get(BOOTSTRAP_KEY)?.value);
|
|
380
|
+
const written = installAwsProfiles(bundle);
|
|
381
|
+
|
|
382
|
+
if (written.length === 0) {
|
|
383
|
+
console.log(
|
|
384
|
+
`no AWS profiles written — ${BOOTSTRAP_KEY} is absent, unparseable, ` +
|
|
385
|
+
`or declares no profile carrying a roleArn.`
|
|
386
|
+
);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
console.log(
|
|
390
|
+
`wrote ${written.length} AWS profile(s): ${written.join(", ")}`
|
|
391
|
+
);
|
|
392
|
+
console.log(
|
|
393
|
+
` Sourced from a long-lived key pair in ~/.aws/credentials. Existing\n` +
|
|
394
|
+
` sections were preserved — only the lisa-managed block was replaced.\n` +
|
|
395
|
+
` Use them explicitly: aws --profile ${written[0]} ...`
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
300
400
|
if (process.argv.includes("--dry-run")) {
|
|
301
401
|
const selected = fetchAll(cfg);
|
|
302
402
|
// Derive here too, or the preview lies in the one place it matters most:
|
|
@@ -314,10 +414,20 @@ function main() {
|
|
|
314
414
|
const { count, derived, dir } = materialize(cfg);
|
|
315
415
|
console.log(`materialized ${count} secret(s) and their notes into ${dir}`);
|
|
316
416
|
if (derived > 0) {
|
|
317
|
-
//
|
|
417
|
+
// Do not claim an override this does not perform.
|
|
418
|
+
//
|
|
419
|
+
// This line used to end "overriding any ambient value". That was true when
|
|
420
|
+
// the key pair was exported and stopped being true when the pair moved into
|
|
421
|
+
// ~/.aws and AWS_PROFILE took its place — AWS_PROFILE loses to ambient
|
|
422
|
+
// environment credentials, it does not beat them. The stale sentence sent
|
|
423
|
+
// two separate investigations after a credential problem that did not
|
|
424
|
+
// exist, while the real fault was that nothing sourced the file. A message
|
|
425
|
+
// that overclaims costs more than no message.
|
|
318
426
|
console.log(
|
|
319
|
-
` ${derived} AWS variable(s) derived from the bootstrap bundle
|
|
320
|
-
`
|
|
427
|
+
` ${derived} AWS variable(s) derived from the bootstrap bundle. ` +
|
|
428
|
+
`These take\n effect only in a shell that sourced the materialized ` +
|
|
429
|
+
`env file; ambient\n credentials outrank AWS_PROFILE, so prefer an ` +
|
|
430
|
+
`explicit --profile.`
|
|
321
431
|
);
|
|
322
432
|
}
|
|
323
433
|
}
|
|
@@ -80,6 +80,51 @@ MCP config so it launches `sonar run mcp`, authenticated from the same env token
|
|
|
80
80
|
|
|
81
81
|
Skip any agent whose CLI is not installed; never wire Kiro or Gemini.
|
|
82
82
|
|
|
83
|
+
## 4a. Patch the generated shims — required, not optional
|
|
84
|
+
|
|
85
|
+
Each integrate writes a five-line shim that pipes the hook payload to `sonar hook
|
|
86
|
+
<event>` and passes the verdict through unchanged. That makes an
|
|
87
|
+
**unauthenticated CLI refuse every prompt and every file read**, with a reason
|
|
88
|
+
string that reads exactly like a real finding:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
SonarQube secret scanning is inactive: not authenticated. Run 'sonar auth login'.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The operator most likely to hit it is the one who just installed the CLI and has
|
|
95
|
+
not logged in — so their very first prompt is rejected, before the session can
|
|
96
|
+
say anything useful. Repoint every generated shim at Lisa's wrapper, which keeps
|
|
97
|
+
real findings blocking, resolves `SONARQUBE_CLI_TOKEN` through
|
|
98
|
+
`lisa-secrets-access` when the CLI reports it has none, and degrades the
|
|
99
|
+
still-unauthenticated case to a warning:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
if ! command -v sonar &> /dev/null; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
here="$(cd -- "$(dirname -- "$0")" && pwd)"
|
|
106
|
+
exec bash "${here}/<relative-path>/scripts/lisa-hooks/sonar-secrets.sh" <vendor-event>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
One wrapper serves every surface — it takes the vendor event name as its only
|
|
110
|
+
argument. Known shim locations and their events:
|
|
111
|
+
|
|
112
|
+
| Agent | Shim | Event |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| Claude | `.claude/hooks/sonar-secrets/build-scripts/prompt-secrets.sh` | `claude-prompt-submit` |
|
|
115
|
+
| Claude | `.claude/hooks/sonar-secrets/build-scripts/pretool-secrets.sh` | `claude-pre-tool-use` |
|
|
116
|
+
| Codex | `.codex/hooks/sonar-secrets/build-scripts/prompt-secrets.sh` | `codex-prompt-submit` |
|
|
117
|
+
| Copilot | `.github/hooks/sonar-secrets/build-scripts/pretool-secrets.sh` | `copilot-pre-tool-use` |
|
|
118
|
+
| Antigravity | `.agents/sonar/hooks/pretool-secrets.sh` | `antigravity-pre-tool-use` |
|
|
119
|
+
|
|
120
|
+
Compute the relative path from each shim's own directory rather than assuming a
|
|
121
|
+
depth — Antigravity's is three levels up, the rest are four.
|
|
122
|
+
|
|
123
|
+
**Re-running an integrate overwrites the shim with the vendor's original**, so
|
|
124
|
+
re-apply this patch whenever you re-run step 4, and say so in the summary. Leave
|
|
125
|
+
a comment at the top of each patched shim naming the wrapper, so the next reader
|
|
126
|
+
does not mistake the delegation for drift.
|
|
127
|
+
|
|
83
128
|
## 5. Write non-secret policy config
|
|
84
129
|
|
|
85
130
|
Merge only non-secret identifiers into committed `.lisa.config.json`, preserving
|