Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .semgrep/BASELINE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Silent-success masking — existing-code baseline

Baseline scan for the custom rules in `silent-success-masking.yaml`
(AI004 / CA-09, [issue #257]). Captured at the rules' introduction;
**40 findings** (15 Python, 25 TypeScript).

[issue #257]: https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/issues/257

The `security:sast:masking` task is **advisory** (it prints findings and
emits SARIF but never fails) until this baseline is triaged. Each finding
must either be fixed (surface the error) or allowlisted with an inline
justified `nosemgrep` comment on the return line:

```
// nosemgrep: ts-silent-success-masking -- <why callers may safely treat this failure as an empty success>
# nosemgrep: py-silent-success-masking -- <why callers may safely treat this failure as an empty success>
```

Once every finding below is resolved, flip the task to blocking by adding
`--error` to the scan command in the root `mise.toml`
(`tasks."security:sast:masking"`) and delete this file.

Regenerate this list with:

```
semgrep scan --config .semgrep/silent-success-masking.yaml --exclude '.semgrep/*' --quiet .
```

## Baseline findings (2026-06-10, branch point `d912ad2`)

### Python (`py-silent-success-masking`)

| Location | Returned default |
|---|---|
| `agent/src/config.py:108` | `""` |
| `agent/src/config.py:130` | `None` |
| `agent/src/config.py:273` | `None` |
| `agent/src/config.py:312` | `""` |
| `agent/src/hooks.py:1064` | `[]` |
| `agent/src/hooks.py:1078` | `[]` |
| `agent/src/hooks.py:1139` | `[]` |
| `agent/src/hooks.py:1193` | `[]` |
| `agent/src/linear_reactions.py:157` | `None` |
| `agent/src/nudge_reader.py:87` | `None` |
| `agent/src/nudge_reader.py:139` | `[]` |
| `agent/src/pipeline.py:123` | `None` |
| `agent/src/post_hooks.py:375` | `None` |
| `agent/src/telemetry.py:471` | `None` |
| `agent/src/telemetry.py:484` | `None` |

### TypeScript (`ts-silent-success-masking`)

| Location | Returned default |
|---|---|
| `cdk/src/handlers/github-webhook-processor.ts:409` | `null` |
| `cdk/src/handlers/github-webhook-processor.ts:435` | `null` |
| `cdk/src/handlers/shared/context-hydration.ts:427` | `null` |
| `cdk/src/handlers/shared/context-hydration.ts:562` | `[]` |
| `cdk/src/handlers/shared/context-hydration.ts:688` | `null` |
| `cdk/src/handlers/shared/github-comment.ts:323` | `null` |
| `cdk/src/handlers/shared/github-webhook-verify.ts:73` | `null` |
| `cdk/src/handlers/shared/linear-feedback.ts:119` | `null` |
| `cdk/src/handlers/shared/linear-issue-lookup.ts:109` | `null` |
| `cdk/src/handlers/shared/linear-issue-lookup.ts:162` | `null` |
| `cdk/src/handlers/shared/linear-oauth-resolver.ts:269` | `null` |
| `cdk/src/handlers/shared/linear-oauth-resolver.ts:347` | `null` |
| `cdk/src/handlers/shared/linear-oauth-resolver.ts:380` | `null` |
| `cdk/src/handlers/shared/linear-verify.ts:68` | `null` |
| `cdk/src/handlers/shared/memory.ts:289` | `undefined` |
| `cdk/src/handlers/shared/preflight.ts:108` | `undefined` |
| `cdk/src/handlers/shared/slack-verify.ts:64` | `null` |
| `cdk/src/handlers/shared/validation.ts:62` | `null` |
| `cdk/src/handlers/shared/validation.ts:166` | `undefined` |
| `cdk/src/handlers/webhook-create-task.ts:56` | `null` |
| `cli/src/commands/linear.ts:1672` | `null` |
| `cli/src/commands/linear.ts:1797` | `null` |
| `cli/src/commands/linear.ts:1905` | `null` |
| `cli/src/commands/slack.ts:361` | `null` |
| `cli/src/config.ts:74` | `null` |

## Triage notes

Some of these are likely *intentional* degraded-mode fallbacks (e.g. the
webhook signature verifiers return `null` for "not verified", and several
Linear/Slack lookups treat upstream outages as "no data"). Those should get
justified `nosemgrep` allowlist comments rather than rewrites — but each one
needs an explicit decision, which is the point of this gate.
122 changes: 122 additions & 0 deletions .semgrep/silent-success-masking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Test fixtures for py-silent-success-masking (run: semgrep test .semgrep/).


def fetch_items() -> list:
raise NotImplementedError


def masked_empty_list() -> list:
try:
return fetch_items()
except Exception:
# ruleid: py-silent-success-masking
return []


def masked_none():
try:
return fetch_items()
except ValueError as exc:
print(exc)
# ruleid: py-silent-success-masking
return None


def masked_empty_dict() -> dict:
try:
return {"items": fetch_items()}
except Exception:
# ruleid: py-silent-success-masking
return {}


def masked_empty_string() -> str:
try:
return str(fetch_items())
except Exception:
# ruleid: py-silent-success-masking
return ""


def masked_dict_constructor() -> dict:
try:
return {"items": fetch_items()}
except Exception:
# ruleid: py-silent-success-masking
return dict()


def masked_bare_except() -> list:
try:
return fetch_items()
except: # noqa: E722
# ruleid: py-silent-success-masking
return []


def ok_reraise() -> list:
try:
return fetch_items()
except Exception as exc:
print(exc)
# ok: py-silent-success-masking
raise


def ok_typed_raise() -> list:
try:
return fetch_items()
except Exception as exc:
# ok: py-silent-success-masking
raise RuntimeError("fetch failed") from exc


def ok_meaningful_fallback() -> dict:
try:
return {"items": fetch_items()}
except Exception:
# ok: py-silent-success-masking
return {"error": True}


def masked_with_finally() -> list:
try:
return fetch_items()
except Exception:
# ruleid: py-silent-success-masking
return []
finally:
print("done")


def masked_multi_except():
try:
return fetch_items()
except ValueError:
# ruleid: py-silent-success-masking
return []
except KeyError:
# ruleid: py-silent-success-masking
return None


# A conditional re-raise does not clear the fallthrough default: callers that
# hit the non-fatal path still cannot distinguish failure from empty success.
def masked_conditional_reraise(fatal: bool) -> list:
try:
return fetch_items()
except Exception:
if fatal:
raise
# ruleid: py-silent-success-masking
return []


def ok_return_in_try_body(items: list) -> list:
try:
if not items:
# ok: py-silent-success-masking
return []
return [i.strip() for i in items]
except Exception as exc:
raise RuntimeError("strip failed") from exc
117 changes: 117 additions & 0 deletions .semgrep/silent-success-masking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Test fixtures for ts-silent-success-masking (run: semgrep test .semgrep/).
/* eslint-disable */

declare function fetchItems(): Promise<string[]>;
declare function parse(s: string): Record<string, unknown>;
declare function log(msg: string): void;

async function maskedEmptyArray(): Promise<string[]> {
try {
return await fetchItems();
} catch (err) {
// ruleid: ts-silent-success-masking
return [];
}
}

function maskedNull(s: string): Record<string, unknown> | null {
try {
return parse(s);
} catch {
// ruleid: ts-silent-success-masking
return null;
}
}

function maskedEmptyObject(s: string): Record<string, unknown> {
try {
return parse(s);
} catch (err) {
log(String(err));
// ruleid: ts-silent-success-masking
return {};
}
}

function maskedUndefined(s: string): Record<string, unknown> | undefined {
try {
return parse(s);
} catch {
// ruleid: ts-silent-success-masking
return undefined;
}
}

function maskedEmptyString(s: string): string {
try {
return JSON.stringify(parse(s));
} catch {
// ruleid: ts-silent-success-masking
return "";
}
}

function okRethrow(s: string): Record<string, unknown> {
try {
return parse(s);
} catch (err) {
log(String(err));
// ok: ts-silent-success-masking
throw err;
}
}

function okTypedThrow(s: string): Record<string, unknown> {
try {
return parse(s);
} catch (err) {
// ok: ts-silent-success-masking
throw new Error(`parse failed: ${String(err)}`);
}
}

function okMeaningfulFallback(s: string): Record<string, unknown> {
try {
return parse(s);
} catch {
// ok: ts-silent-success-masking
return { error: true };
}
}

function maskedWithFinally(s: string): string[] {
try {
return [s];
} catch {
// ruleid: ts-silent-success-masking
return [];
} finally {
log("done");
}
}

// A conditional rethrow does not clear the fallthrough default: callers that
// hit the non-fatal path still cannot distinguish failure from empty success.
function maskedConditionalRethrow(s: string): Record<string, unknown> | null {
try {
return parse(s);
} catch (err) {
if (s.length > 0) {
throw err;
}
// ruleid: ts-silent-success-masking
return null;
}
}

function okReturnInTryBody(items: string[]): string[] {
try {
if (items.length === 0) {
// ok: ts-silent-success-masking
return [];
}
return items.map((i) => i.trim());
} catch (err) {
throw new Error(`trim failed: ${String(err)}`);
}
}
Loading
Loading