Sibling of #44. Same defect class: a mention of a credential path is treated as a read of one. Different entry point, so it needs its own fix.
What happens
get_mitre_mapping runs content upgrades against the evidence text. Masking of quoted literals is applied only when the evidence is shell text:
https://github.com/blitzcrieg1/agentmetry/blob/master/apps/orchestrator/agentmetry/core/audit/mitre.py#L161
def _shell_text(evidence):
if isinstance(evidence, str):
return evidence
if isinstance(evidence, dict):
command = evidence.get("command")
if isinstance(command, str) and command:
return command
return None
For a dict with no command key, _shell_text returns None, so literal = text and nothing is masked. The docstring explains why, and the reasoning is correct as far as it goes: a tool call carrying {"path": "~/.aws/credentials"} has no shell quoting to reason about, and its quotes are JSON syntax rather than an author's intent.
That holds when the argument is a path. It breaks when the argument is a program that happens to name one.
Observed
A browser automation call driving the demonstration lab:
{
"name": "mcp__Claude_Browser__javascript_tool",
"arguments": {
"action": "javascript_exec",
"tabId": "seed",
"text": "(() => {\n const sb = document.querySelector('#sandbox');\n const chip = (txt) => [...sb.querySelectorAll('button')].find(b => b.innerText.includes(txt)).click();\n chip('POST to external host');\n chip('read ~/.aws/credentials');\n return 'egress then credential read queued';\n})()"
},
"mitre": {
"tactic_id": "TA0006",
"tactic": "Credential Access",
"technique_id": "T1552.001",
"technique": "Credentials In Files"
}
}
~/.aws/credentials here is a button label on a web page. The call clicked a button. It opened no file, and the string never left the browser tab.
CREDENTIAL_PATH matched it and upgraded the whole call to Credential Access. A git push seven minutes later then paired with it to produce:
dotfile-read-then-git-push CRITICAL "Credential read followed by git push"
A critical exfiltration finding, over a button click and a branch push.
Why it matters beyond this instance
The demo page is a self-inflicted case, but the general shape is not rare. Any MCP tool whose argument is a body of text rather than a filesystem target hits this: browser automation, an editor writing documentation that names ~/.ssh/id_rsa, a code search for .env, a chat tool quoting a runbook.
The current behavior means writing about credential handling is indistinguishable from doing it, and the resulting finding is critical severity.
Severity is what makes it worth fixing rather than tolerating. A noisy medium is an annoyance. A critical that fires on documentation is the finding an operator learns to dismiss, and then dismisses the true positive with it.
Direction
The distinction is not shell versus structured. It is target versus content.
An argument like path, file, file_path, cwd or command names something the tool acts on. An argument like text, body, content, query, message or prompt is payload the tool carries. The first should be matched against credential patterns. The second should not, or should downgrade rather than upgrade.
Keying on the argument name is imperfect and MCP servers are free to name things however they like, but it is far better than the current all-or-nothing, and it preserves the correct behavior for {"path": "~/.aws/credentials"} that the existing docstring is protecting.
Worth resolving together with #44, since both are the mention-versus-action confusion and a shared fix may cover them.
Blocked until the freeze lifts
core/audit/mitre.py is a ruleset fingerprint input. Editing it restarts the dogfood clock, which is at 1 of 4 green weeks. Hold with #44 and land both first once the gate closes.
Sibling of #44. Same defect class: a mention of a credential path is treated as a read of one. Different entry point, so it needs its own fix.
What happens
get_mitre_mappingruns content upgrades against the evidence text. Masking of quoted literals is applied only when the evidence is shell text:https://github.com/blitzcrieg1/agentmetry/blob/master/apps/orchestrator/agentmetry/core/audit/mitre.py#L161
For a dict with no
commandkey,_shell_textreturnsNone, soliteral = textand nothing is masked. The docstring explains why, and the reasoning is correct as far as it goes: a tool call carrying{"path": "~/.aws/credentials"}has no shell quoting to reason about, and its quotes are JSON syntax rather than an author's intent.That holds when the argument is a path. It breaks when the argument is a program that happens to name one.
Observed
A browser automation call driving the demonstration lab:
{ "name": "mcp__Claude_Browser__javascript_tool", "arguments": { "action": "javascript_exec", "tabId": "seed", "text": "(() => {\n const sb = document.querySelector('#sandbox');\n const chip = (txt) => [...sb.querySelectorAll('button')].find(b => b.innerText.includes(txt)).click();\n chip('POST to external host');\n chip('read ~/.aws/credentials');\n return 'egress then credential read queued';\n})()" }, "mitre": { "tactic_id": "TA0006", "tactic": "Credential Access", "technique_id": "T1552.001", "technique": "Credentials In Files" } }~/.aws/credentialshere is a button label on a web page. The call clicked a button. It opened no file, and the string never left the browser tab.CREDENTIAL_PATHmatched it and upgraded the whole call to Credential Access. Agit pushseven minutes later then paired with it to produce:A critical exfiltration finding, over a button click and a branch push.
Why it matters beyond this instance
The demo page is a self-inflicted case, but the general shape is not rare. Any MCP tool whose argument is a body of text rather than a filesystem target hits this: browser automation, an editor writing documentation that names
~/.ssh/id_rsa, a code search for.env, a chat tool quoting a runbook.The current behavior means writing about credential handling is indistinguishable from doing it, and the resulting finding is critical severity.
Severity is what makes it worth fixing rather than tolerating. A noisy medium is an annoyance. A critical that fires on documentation is the finding an operator learns to dismiss, and then dismisses the true positive with it.
Direction
The distinction is not shell versus structured. It is target versus content.
An argument like
path,file,file_path,cwdorcommandnames something the tool acts on. An argument liketext,body,content,query,messageorpromptis payload the tool carries. The first should be matched against credential patterns. The second should not, or should downgrade rather than upgrade.Keying on the argument name is imperfect and MCP servers are free to name things however they like, but it is far better than the current all-or-nothing, and it preserves the correct behavior for
{"path": "~/.aws/credentials"}that the existing docstring is protecting.Worth resolving together with #44, since both are the mention-versus-action confusion and a shared fix may cover them.
Blocked until the freeze lifts
core/audit/mitre.pyis a ruleset fingerprint input. Editing it restarts the dogfood clock, which is at 1 of 4 green weeks. Hold with #44 and land both first once the gate closes.