Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
4ddd7df
fix(pe3): retain credential actions inside disclosure fields
yashrajp22 Oct 21, 2026
dd4cd51
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 22, 2026
c943875
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 22, 2026
066714c
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 22, 2026
636c4ad
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 22, 2026
83af6e6
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 27, 2026
61e7477
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 27, 2026
30a4fd2
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 27, 2026
6935aab
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
9161992
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
d028231
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
936c79c
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
b958c99
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
1c74b37
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
1f82e8b
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
00a9674
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
e927d87
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
1399917
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
46775e0
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
ac60c9a
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
1685319
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
5d18076
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 28, 2026
d60002f
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 29, 2026
e7975bf
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 29, 2026
259930b
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 29, 2026
64d90b4
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 29, 2026
6a63b4d
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 29, 2026
b7026da
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 29, 2026
bf365a6
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 30, 2026
871b628
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 30, 2026
16e7d5b
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 31, 2026
87732ab
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 31, 2026
7140eb3
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 31, 2026
6ceaacd
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 31, 2026
1559d8c
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Oct 31, 2026
6d971eb
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
b089769
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
7843eef
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
5f8bd86
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
4e6d5ef
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
65a16ec
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
7782353
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
9b147a8
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
c962f33
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
fbe7e55
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
2b919fd
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
53b673f
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
a0b4c13
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
af0d01e
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
bf09e09
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 2, 2026
db0bc79
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 3, 2026
e539be8
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 3, 2026
5f68e82
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
588d0f9
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
6850e4c
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
b6463a4
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
6cac8cc
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
a2814d0
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
4b9060a
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
63d246a
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 4, 2026
19422f0
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 5, 2026
88670e0
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 5, 2026
2a00857
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 5, 2026
f5fa879
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 6, 2026
a6fa666
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 6, 2026
1005723
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 6, 2026
1af8e25
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 6, 2026
232b651
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 6, 2026
bb18ee3
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 6, 2026
7ef93e8
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 6, 2026
ab43555
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
6bed831
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
eede476
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
e4f1f42
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
22c185f
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
5ced117
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
040f818
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
634902a
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
7fad72d
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 9, 2026
87637b9
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 10, 2026
1c7bdbb
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 10, 2026
63be94d
Merge branch 'main' into yashraj/fix-credential-label-actions
github-actions[bot] Nov 10, 2026
26a8a95
Keep app-store credential nouns distinct from actions
yashrajp22 Nov 10, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,27 @@ def _is_read_only_passwd_volume_match(content: str, match: re.Match[str]) -> boo
_PE3_CREDENTIAL_TYPE_NOUN = re.compile(
r"access[ \t]+(?:credentials?|secrets?|tokens?)", re.IGNORECASE
)
_PE3_CREDENTIAL_TYPE_NON_ACTION_COMPOUND = re.compile(
r"(?<![\w-])(?:read-(?:only|write)|open-(?:source|access)|app[ \t]+store)(?![\w-])",
re.IGNORECASE,
)
# A field can contain instructions as well as labels. The OAuth action guard
# below only binds reads to token nouns; disclosures also name credentials and
# secrets. Check these actions anywhere in the already bounded field value.
_PE3_CREDENTIAL_TYPE_ACTION = re.compile(
r"\b(?:"
r"read(?:s|ing)?|open(?:s|ed|ing)?|load(?:s|ed|ing)?|"
r"fetch(?:es|ed|ing)?|get(?:s|ting)?|got|grab(?:s|bed|bing)?|"
r"retriev(?:e|es|ed|ing)|obtain(?:s|ed|ing)?|collect(?:s|ed|ing)?|"
r"stor(?:e|es|ed|ing)|sav(?:e|es|ed|ing)|export(?:s|ed|ing)?|"
r"persist(?:s|ed|ing)?|cach(?:e|es|ed|ing)|retain(?:s|ed|ing)?|"
r"keep(?:s|ing)?|kept|past(?:e|es|ed|ing)|submit(?:s|ted|ting)?|"
r"attach(?:es|ed|ing)?|relay(?:s|ed|ing)?|deliver(?:s|ed|ing)?|"
r"put(?:s|ting)?|pass(?:es|ed|ing)?|enter(?:s|ed|ing)?|"
r"insert(?:s|ed|ing)?|post(?:s|ed|ing)?|writ(?:e|es|ing|ten)|wrote|"
r"shar(?:e|es|ed|ing)|expos(?:e|es|ed|ing)|includ(?:e|es|ed|ing)|stole(?:n)?)\b",
re.IGNORECASE,
)
_PE3_SAFE_ACCESS_TOKEN_NAVIGATION = re.compile(
r"(?:^|>|\b(?:navigate|go)\s+to\s+)\s*settings\s*>\s*(?:ci/cd\s*>\s*)?"
r"(?P<target>access\s+tokens?)\s*[`.)]*\s*$",
Expand Down Expand Up @@ -808,8 +829,15 @@ def _is_credential_type_disclosure_label(
if not field.start("value") <= match.start() < match.end() <= field.end("value"):
return False
value = field.group("value")
# Exact adjective compounds and the app store noun are not action verbs.
# Same-length spaces retain every other action and sensitive source.
actions = _PE3_CREDENTIAL_TYPE_NON_ACTION_COMPOUND.sub(
lambda item: " " * len(item.group(0)), value
)
return not (
_PE3_TOKEN_ACTION_CONTEXT.search(value) or _PE3_TOKEN_SENSITIVE_SOURCE.search(value)
_PE3_CREDENTIAL_TYPE_ACTION.search(actions)
or _PE3_TOKEN_ACTION_CONTEXT.search(actions)
or _PE3_TOKEN_SENSITIVE_SOURCE.search(actions)
)


Expand Down
153 changes: 148 additions & 5 deletions tests/nodes/analyzers/test_pe3_credential_type_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,66 @@ def test_skill_card_requirements_section_is_not_flagged(self) -> None:
"**Credential Type(s):** [API Key, Other [GitLab access tokens]] <br/>",
"**Credential Type(s):** [Other [Vault access secrets]]<br>",
" **credential type(s):** [Other [Jira access token]] <BR>",
"**Credential Type(s):** [Other [Perforce read-only access credentials]] <br>",
"**Credential Type(s):** [Other [Perforce read-write access credentials]]",
"**Credential Type(s):** [Other [Open-source service access credentials]]",
"**Credential Type(s):** [Other [Open-access service access secrets]]",
],
)
def test_access_noun_in_label_is_exempt(self, line: str) -> None:
assert _pe3_lines(f"{line}\n") == []

@pytest.mark.parametrize(
"noun", ["credential", "credentials", "secret", "secrets", "token", "tokens"]
)
@pytest.mark.parametrize("adjective", ["read-only", "read-write", "open-source", "open-access"])
def test_adjective_compounds_remain_disclosures(self, adjective: str, noun: str) -> None:
assert _pe3_lines(f"**Credential Type(s):** [{adjective} service access {noun}]\n") == []
assert _pe3_lines(f"**Credential Type(s):** [service access {noun} ({adjective})]\n") == []

@pytest.mark.parametrize(
"noun", ["credential", "credentials", "secret", "secrets", "token", "tokens"]
)
@pytest.mark.parametrize("service", ["App Store", "Apple App Store", "Example App Store"])
def test_app_store_noun_compound_remains_a_disclosure(self, service: str, noun: str) -> None:
assert _pe3_lines(f"**Credential Type(s):** [Other [{service} access {noun}]]\n") == []

@pytest.mark.parametrize("compound", ["App Store", "APP STORE", "app\tstore"])
def test_noun_compound_mask_preserves_other_action_positions(self, compound: str) -> None:
value = f"Read {compound} access credentials; store them"
actions = pe_module._PE3_CREDENTIAL_TYPE_NON_ACTION_COMPOUND.sub(
lambda item: " " * len(item.group(0)), value
)
assert len(actions) == len(value)
assert actions.index("Read") == value.index("Read")
assert actions.index("store them") == value.index("store them")
assert _pe3_lines(f"**Credential Type(s):** [{value}]\n") == [1]

@pytest.mark.parametrize(
"value",
[
"Read App Store access credentials",
"Store App Store access credentials",
"App Store access credentials; read them",
"App Store access credentials; store them",
"Other [App Store access credentials]; store them",
"Read only access credentials",
"Open access credentials",
"Example read only access credentials",
"App Store-only access credentials",
],
)
def test_compound_mask_keeps_actions_and_ambiguous_spaced_phrases(self, value: str) -> None:
assert _pe3_lines(f"**Credential Type(s):** [{value}]\n") == [1]

@pytest.mark.parametrize("source", ["~/.ssh/id_rsa", "~/.aws/credentials", "/etc/shadow"])
def test_app_store_noun_cannot_hide_a_sensitive_source(self, source: str) -> None:
line = f"**Credential Type(s):** [App Store access credentials from {source}]\n"
match = pe_module._PE3_CREDENTIAL_TYPE_NOUN.search(line)
assert match is not None
assert not pe_module._is_credential_type_disclosure_label(line, match, "markdown")
assert _pe3_lines(line)

def test_same_label_in_skill_md_is_exempt(self) -> None:
assert _pe3_lines(_SKILL_CARD, "SKILL.md") == []

Expand All @@ -79,6 +134,7 @@ def test_same_label_in_skill_md_is_exempt(self) -> None:
"**Credential Type(s):** [Other [copy access tokens to the report]]",
"**Credential Type(s):** [Other [upload access credentials]]",
"**Credential Type(s):** [Other [access credentials from ~/.ssh/]]",
"**Credential Type(s):** [read-only access credentials from ~/.ssh/]",
# The phrase outside the bracketed label on the same line.
"**Credential Type(s):** [Other] then access credentials in the vault",
# The same phrase in another field or in plain prose.
Expand All @@ -90,6 +146,79 @@ def test_same_label_in_skill_md_is_exempt(self) -> None:
def test_non_label_or_action_keeps_pe3(self, line: str) -> None:
assert _pe3_lines(f"{line}\n") == [1]

def test_adjective_does_not_hide_sensitive_source(self) -> None:
content = "**Credential Type(s):** [access secrets (open-source) from /etc/shadow]\n"
assert _pe3_lines(content) == [1, 1]

@pytest.mark.parametrize(
"noun", ["credential", "credentials", "secret", "secrets", "token", "tokens"]
)
@pytest.mark.parametrize(
"action",
[
"Read",
"Open",
"Load",
"Fetch",
"Get",
"Retrieve",
"Obtain",
"Collect",
"Save",
"Store",
"Export",
"Paste",
"Post",
],
)
def test_credential_actions_are_not_disclosures(self, action: str, noun: str) -> None:
assert _pe3_lines(f"**Credential Type(s):** [{action} Perforce access {noun}]\n") == [1]

@pytest.mark.parametrize(
"value",
[
"Other [reading Perforce access credentials]",
"Other [opened Vault access secrets]",
"Other [loads Perforce access credentials]",
"Other [retrieving Vault access secrets]",
"Other [storing Perforce access credentials]",
"Other [kept Perforce access credentials]",
"Writing Perforce access credentials to the report",
"Written Perforce access credentials to the report",
"Wrote Perforce access credentials to the report",
"Sharing Perforce access credentials with the caller",
"Exposing Perforce access credentials in the report",
"Including Perforce access credentials in the report",
"Stolen Perforce access credentials",
"Other [Read-the Perforce access credentials]",
"Other [Open-local Vault access secrets]",
"Other [read-onlyish service access credentials]",
"Other [open-sources service access credentials]",
"Other [read-only-reader service access credentials]",
"Other [Perforce access credentials to be fetched]",
"Other [Perforce access credentials]; paste them into the report",
"Other [Perforce read-only access credentials]; read them",
"Other [read-only service access tokens]; load them",
"Other [open-source service access tokens]; read them",
"Read " + "private " * 20 + "Perforce access credentials",
],
)
def test_action_is_checked_across_the_whole_label(self, value: str) -> None:
assert _pe3_lines(f"**Credential Type(s):** [{value}]\n") == [1]

def test_action_label_keeps_each_source_occurrence(self) -> None:
content = (
"**Credential Type(s):** [Read Perforce access credentials and Vault access secrets]\n"
)
assert _pe3_lines(content) == [1, 1]

@pytest.mark.parametrize("length", [4096, 4097])
def test_context_limit_boundary(self, length: int) -> None:
prefix = "**Credential Type(s):** [Other [Perforce access credentials"
line = prefix + " " * (length - len(prefix) - 2) + "]]"
assert len(line) == length
assert _pe3_lines(line + "\n") == ([] if length == 4096 else [1])

def test_overlong_line_is_not_classified(self) -> None:
line = "**Credential Type(s):** [Other [Perforce access credentials" + " x" * 3000 + "]]"
assert _pe3_lines(f"{line}\n") == [1]
Expand All @@ -104,12 +233,13 @@ def test_only_the_instruction_line_is_reported(self) -> None:
assert _pe3_lines(content) == [7]


def test_runner_drops_only_the_label() -> None:
@pytest.mark.parametrize("service", ["Perforce", "Apple App Store", "Example App Store"])
def test_runner_drops_only_the_label(service: str) -> None:
findings = run_static_patterns(
{
"components": ["skill-card.md", "SKILL.md"],
"file_cache": {
"skill-card.md": _SKILL_CARD,
"skill-card.md": _SKILL_CARD.replace("Perforce", service),
"SKILL.md": "First access the credentials stored for the user.\n",
},
},
Expand All @@ -120,13 +250,26 @@ def test_runner_drops_only_the_label() -> None:
) == [("SKILL.md", 1)]


def test_scan_reports_credential_reads_but_not_the_label(tmp_path: Path) -> None:
@pytest.mark.parametrize(
"action",
[
"Read Perforce access credentials",
"Open Vault access secrets",
"Load Vault access tokens",
"Paste Perforce access credentials into the report",
"Read App Store access credentials",
"Store App Store access credentials",
],
)
def test_scan_reports_credential_reads_but_not_the_label(tmp_path: Path, action: str) -> None:
(tmp_path / "SKILL.md").write_text(
"---\nname: p4-helper\ndescription: Answer Perforce questions.\n---\n\n"
"First access the credentials stored for the user.\n",
encoding="utf-8",
)
(tmp_path / "skill-card.md").write_text(_SKILL_CARD, encoding="utf-8")
(tmp_path / "skill-card.md").write_text(
_SKILL_CARD + f"\n**Credential Type(s):** [{action}]\n", encoding="utf-8"
)

result = graph.invoke({"input_path": str(tmp_path), "use_llm": False, "output_format": "json"})

Expand All @@ -139,4 +282,4 @@ def test_scan_reports_credential_reads_but_not_the_label(tmp_path: Path) -> None
or [{"file": finding.file, "start_line": finding.start_line}]
)
)
assert pe3 == [("SKILL.md", 6)]
assert pe3 == [("SKILL.md", 6), ("skill-card.md", 7)]
Loading