Skip to content

Commit 5a52a88

Browse files
bgagentcursoragent
andcommitted
feat(security): flip silent-success masking gate to blocking (#257)
Triage all 40 baseline AI004 findings with justified inline nosemgrep allowlists for intentional fail-open and degraded-mode fallbacks, add --error to security:sast:masking, and remove the advisory baseline doc. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 26f119f commit 5a52a88

27 files changed

Lines changed: 44 additions & 116 deletions

.semgrep/BASELINE.md

Lines changed: 0 additions & 87 deletions
This file was deleted.

.semgrep/silent-success-masking.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# even though it does not mask the outer failure. Rare in practice; use the
1919
# nosemgrep allowlist if it occurs.
2020
#
21-
# Run: mise run security:sast:masking (advisory; emits SARIF)
21+
# Run: mise run security:sast:masking (blocking; emits SARIF)
2222
# Test: semgrep test .semgrep/
2323
rules:
2424
- id: ts-silent-success-masking

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Run `mise tasks --all` (with `MISE_EXPERIMENTAL=1`) for the full list. Common co
111111
- **`mise //docs:build`** — Sync and build docs site.
112112
- **`mise run security:secrets`** — Gitleaks at repo root.
113113
- **`mise run security:sast`** — Semgrep on the repo (root; includes **`agent/`** Python among paths).
114-
- **`mise run security:sast:masking`** — Custom semgrep rules for silent-success masking (`catch`/`except` returning empty defaults, AI004). Advisory while the baseline in **`.semgrep/BASELINE.md`** is open; emits SARIF to `test-reports/`. Allowlist intentional fallbacks with an inline justified `nosemgrep: <rule-id> -- <reason>` comment.
114+
- **`mise run security:sast:masking`** — Custom semgrep rules for silent-success masking (`catch`/`except` returning empty defaults, AI004). Blocking; emits SARIF to `test-reports/`. Allowlist intentional fallbacks with an inline justified `nosemgrep: <rule-id> -- <reason>` comment.
115115
- **`mise run security:deps`** — OSV Scanner on **`yarn.lock`** (all JS workspaces) and **`agent/uv.lock`**.
116116
- **`mise run security`** — Runs **`security:secrets`**, **`security:deps`**, **`security:sast`**, **`security:sast:masking`**, **`security:grype`**, **`security:retire`**, **`security:gh-actions`**, and **`//agent:security`**.
117117
- **`mise run security:retire`** — Retire.js on CDK, CLI, and docs packages.

agent/src/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def resolve_linear_api_token(channel_metadata: dict[str, str] | None = None) ->
105105
from botocore.exceptions import BotoCoreError, ClientError
106106
except ImportError as e:
107107
log("WARN", f"resolve_linear_api_token: boto3 unavailable ({e}); skipping")
108+
# nosemgrep: py-silent-success-masking -- optional Linear MCP; boto3 unavailable
108109
return ""
109110

110111
sm = boto3.client("secretsmanager", region_name=region)
@@ -127,6 +128,7 @@ def _fetch_token() -> dict | None:
127128
f"resolve_linear_api_token: secret '{secret_arn}' is not valid JSON "
128129
f"({type(e).__name__}: {e}); workspace requires re-onboarding",
129130
)
131+
# nosemgrep: py-silent-success-masking -- corrupt OAuth JSON; None means no token
130132
return None
131133

132134
def _is_expiring(expires_at_iso: str, threshold_seconds: int = 60) -> bool:
@@ -270,6 +272,7 @@ def _refresh(current: dict) -> dict | None:
270272
fresh = _fetch_token()
271273
except (ClientError, BotoCoreError) as e:
272274
log("WARN", f"resolve_linear_api_token: re-read after invalid_grant failed: {e}")
275+
# nosemgrep: py-silent-success-masking -- transient SM re-read after invalid_grant
273276
return None
274277
if fresh is None:
275278
# Secret is unreadable (corrupted JSON). Already logged inside
@@ -309,6 +312,7 @@ def _refresh(current: dict) -> dict | None:
309312
is_hard_failure = code in ("AccessDeniedException", "ResourceNotFoundException")
310313
severity = "ERROR" if is_hard_failure else "WARN"
311314
log(severity, f"resolve_linear_api_token failed: {type(e).__name__}: {e}")
315+
# nosemgrep: py-silent-success-masking -- SM fetch logged; empty token disables Linear
312316
return ""
313317
if token_obj is None:
314318
# Corrupted secret JSON; already logged inside _fetch_token.

agent/src/hooks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,7 @@ def _nudge_between_turns_hook(ctx: dict) -> list[str]:
10881088
pending = nudge_reader.read_pending(task_id)
10891089
except Exception as exc:
10901090
log("WARN", f"nudge read_pending raised: {type(exc).__name__}: {exc}")
1091+
# nosemgrep: py-silent-success-masking -- fail-open hook; DDB blip must not block agent
10911092
return []
10921093

10931094
# Filter out any nudges already injected in this process (regardless of
@@ -1102,6 +1103,7 @@ def _nudge_between_turns_hook(ctx: dict) -> list[str]:
11021103
formatted = nudge_reader.format_as_user_message(pending)
11031104
except Exception as exc:
11041105
log("WARN", f"nudge format failed: {type(exc).__name__}: {exc}")
1106+
# nosemgrep: py-silent-success-masking -- fail-open hook; bad nudge must not block agent
11051107
return []
11061108

11071109
# Record injection BEFORE mark_consumed so a persistent mark_consumed
@@ -1163,6 +1165,7 @@ def _denial_between_turns_hook(ctx: dict) -> list[str]:
11631165
pending = engine.drain_denial_injections()
11641166
except Exception as exc: # pragma: no cover — defensive
11651167
log("WARN", f"denial drain raised: {type(exc).__name__}: {exc}")
1168+
# nosemgrep: py-silent-success-masking -- fail-open hook; denial injection is best-effort
11661169
return []
11671170
if not pending:
11681171
return []
@@ -1217,6 +1220,7 @@ def _cancel_between_turns_hook(ctx: dict) -> list[str]:
12171220
record = task_state.get_task(task_id)
12181221
except task_state.TaskFetchError as exc:
12191222
log("WARN", f"cancel hook get_task raised: {type(exc).__name__}: {exc}")
1223+
# nosemgrep: py-silent-success-masking -- fail-open cancel; DDB blip delays one turn
12201224
return []
12211225
if record and record.get("status") == "CANCELLED":
12221226
ctx["_cancel_requested"] = True

agent/src/linear_reactions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def _graphql(query: str, variables: dict[str, Any]) -> dict[str, Any] | None:
155155
)
156156
except requests.RequestException as e:
157157
log("WARN", f"linear_reactions: request failed ({type(e).__name__}): {e}")
158+
# nosemgrep: py-silent-success-masking -- Linear reactions are best-effort on network blips
158159
return None
159160

160161
if resp.status_code in (401, 403):

agent/src/nudge_reader.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def _get_table() -> Any | None:
8484
return _TABLE_CACHE
8585
except Exception as exc:
8686
log("WARN", f"Failed to init nudge DDB table: {type(exc).__name__}: {exc}")
87+
# nosemgrep: py-silent-success-masking -- nudge table init failure; read_pending returns []
8788
return None
8889

8990

@@ -136,6 +137,7 @@ def read_pending(task_id: str, table: Any | None = None) -> list[PendingNudge]:
136137
break
137138
except Exception as exc:
138139
log("WARN", f"Nudge DDB query failed: {type(exc).__name__}: {exc}")
140+
# nosemgrep: py-silent-success-masking -- DDB query failure yields no pending nudges
139141
return []
140142

141143
if truncated:

agent/src/pipeline.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def _maybe_upload_trace(
120120
artifact = trajectory.dump_gzipped_jsonl()
121121
except Exception as e:
122122
log("WARN", f"Trace dump_gzipped_jsonl failed: {type(e).__name__}: {e}")
123+
# nosemgrep: py-silent-success-masking -- trace upload best-effort; missing artifact ok
123124
return None
124125
if not artifact:
125126
log(

agent/src/post_hooks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,5 @@ def _extract_agent_notes(repo_dir: str, branch: str, config: TaskConfig) -> str
433433
return None
434434
except Exception as e:
435435
log("WARN", f"Failed to extract agent notes from PR body: {type(e).__name__}: {e}")
436+
# nosemgrep: py-silent-success-masking -- PR body notes optional; extraction failure logged
436437
return None

agent/src/telemetry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ def upload_trace_to_s3(
471471
return f"s3://{bucket}/{key}"
472472
except ImportError:
473473
print("[trace/upload] boto3 not available — skipping", flush=True)
474+
# nosemgrep: py-silent-success-masking -- trace upload optional; missing boto3 skips upload
474475
return None
475476
except Exception as e:
476477
exc_type = type(e).__name__
@@ -484,6 +485,7 @@ def upload_trace_to_s3(
484485
"[trace/upload] WARNING: IAM misconfiguration likely — trace artifact is lost.",
485486
flush=True,
486487
)
488+
# nosemgrep: py-silent-success-masking -- S3 trace upload best-effort; failure logged
487489
return None
488490

489491

0 commit comments

Comments
 (0)