Skip to content

Commit 6ce6d48

Browse files
committed
fix(comments): close ignore authorization gaps
1 parent dd02c76 commit 6ce6d48

6 files changed

Lines changed: 77 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
- On GitHub this is read from the effective repository permission and cached per
5959
commenter for the run. Write, maintain, or admin access is required; relationship
6060
labels such as `MEMBER` and `COLLABORATOR` are not treated as permissions.
61+
- A 404 from GitHub's collaborator-permission endpoint is treated as a definitive
62+
denial rather than an unreadable permission, so the default `enforce` policy does
63+
not honor ignore commands from users outside the repository.
6164
- GitLab notes carry no equivalent field, so project membership is read once per
6265
run (only when an ignore command is present) and Developer or above is required.
6366
If that lookup cannot be answered — a `CI_JOB_TOKEN` generally cannot read the
@@ -92,6 +95,8 @@
9295
accept scoped packages while remaining compatible with older bare-name replies.
9396
A leading npm scope is no longer mistaken for an ecosystem, so
9497
`ignore @types/node@*` no longer also ignores the package named `node`.
98+
- Ignore telemetry uses the same package matcher as alert suppression, so legacy
99+
bare-name commands generate an event for the alert they suppress.
95100
- Dependency overviews preserve added, updated, removed, and replaced package
96101
classifications instead of presenting updates as new dependencies. Added and
97102
updated rows keep their diff badge; removed and replaced, which have no

socketsecurity/core/scm/github.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from socketsecurity import USER_AGENT
99
from socketsecurity.core import log
1010
from socketsecurity.core.classes import Comment
11+
from socketsecurity.core.exceptions import APIFailure
1112
from socketsecurity.core.git_remote import parse_git_remote
1213
from socketsecurity.core.scm_comments import Comments
1314
from socketsecurity.socketcli import CliClient
@@ -273,6 +274,17 @@ def is_ignore_authorized(self, comment: Comment) -> bool:
273274
permission = (
274275
result["permission"].casefold() in self.WRITE_PERMISSIONS
275276
)
277+
except APIFailure as error:
278+
if getattr(error, "status_code", None) == 404:
279+
# The repository was readable when its comments were listed,
280+
# so a missing collaborator permission is a definitive denial.
281+
permission = False
282+
else:
283+
log.warning(
284+
"Could not read GitHub repository permission for "
285+
f"{author}: {error}"
286+
)
287+
permission = None
276288
except Exception as error:
277289
log.warning(
278290
f"Could not read GitHub repository permission for {author}: {error}"

socketsecurity/socketcli.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,30 @@ def should_write_comment(disabled: bool, has_findings: bool, update_existing: bo
138138
return update_existing
139139
return True
140140

141+
142+
def _match_ignored_alerts_for_telemetry(
143+
ignored_alerts: list,
144+
ignore_all: bool,
145+
ignore_commands: list[tuple[str, str]],
146+
) -> list:
147+
"""Return alerts attributable to one ignore comment."""
148+
if ignore_all:
149+
return list(ignored_alerts)
150+
return [
151+
alert
152+
for alert in ignored_alerts
153+
if any(
154+
Comments.is_ignore(
155+
alert.pkg_name,
156+
alert.pkg_version,
157+
name,
158+
version,
159+
alert.pkg_type,
160+
)
161+
for name, version in ignore_commands
162+
)
163+
]
164+
141165
def _select_pull_request_provider(integration_type: str, scm_type: str) -> str:
142166
"""Prefer an active comment adapter when resolving pull request context."""
143167
return scm_type if scm_type in ("github", "gitlab") else integration_type
@@ -837,16 +861,11 @@ def _is_unprocessed(c):
837861
sender_id = str(user.get("id", ""))
838862

839863
# Match this comment's targets to the actual ignored alerts
840-
matched_alerts = []
841-
if c_ignore_all:
842-
matched_alerts = ignored_alerts
843-
else:
844-
for alert in ignored_alerts:
845-
full_name = f"{alert.pkg_type}/{alert.pkg_name}"
846-
purl = (full_name, alert.pkg_version)
847-
purl_star = (full_name, "*")
848-
if purl in c_ignore_commands or purl_star in c_ignore_commands:
849-
matched_alerts.append(alert)
864+
matched_alerts = _match_ignored_alerts_for_telemetry(
865+
ignored_alerts,
866+
c_ignore_all,
867+
c_ignore_commands,
868+
)
850869

851870
shared_fields = {
852871
"event_kind": "user-action",

tests/unit/test_git_remote.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
("gitlab.example.com", "acme/platform/widgets"),
2626
),
2727
("git://github.com/acme/widgets.git", ("github.com", "acme/widgets")),
28-
# Cosmetic variation callers should not have to normalise themselves.
28+
# Cosmetic variation callers should not have to normalize themselves.
2929
(" https://github.com/acme/widgets/ ", ("github.com", "acme/widgets")),
3030
# Credentials in the URL must not leak into the host.
3131
("https://user@github.com/acme/widgets", ("github.com", "acme/widgets")),

tests/unit/test_ignore_authorization.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111

1212
from socketsecurity.core.classes import Comment
13+
from socketsecurity.core.exceptions import APIFailure
1314
from socketsecurity.core.scm.github import Github
1415
from socketsecurity.core.scm.gitlab import Gitlab
1516
from socketsecurity.core.scm_comments import Comments
@@ -69,6 +70,17 @@ def test_github_permission_is_fetched_once_per_commenter():
6970
assert github.calls == ["repos/o/r/collaborators/maintainer/permission"]
7071

7172

73+
def test_github_404_is_a_cached_unauthorized_result():
74+
github = _github(
75+
raises=APIFailure("not a collaborator", status_code=404),
76+
)
77+
comment = _comment(user={"login": "outsider"})
78+
79+
assert github.is_ignore_authorized(comment) is False
80+
assert github.is_ignore_authorized(comment) is False
81+
assert github.calls == ["repos/o/r/collaborators/outsider/permission"]
82+
83+
7284
def test_github_unreadable_permission_honors_the_command_with_a_warning(caplog):
7385
github = _github(raises=Exception("403 Forbidden"))
7486

tests/unit/test_ignore_telemetry_filtering.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Tests for the +1 reaction dedup logic used to filter ignore comments for telemetry."""
22

3+
from types import SimpleNamespace
34
from unittest.mock import Mock
45

56
from socketsecurity.core.classes import Comment
67
from socketsecurity.core.scm_comments import Comments
8+
from socketsecurity.socketcli import _match_ignored_alerts_for_telemetry
79

810

911
def _make_comment(body: str, thumbs_up: int = 0, comment_id: int = 1, user: dict | None = None) -> Comment:
@@ -161,6 +163,22 @@ def test_no_unprocessed_means_no_telemetry(self):
161163
assert len(unprocessed) == 0
162164

163165

166+
def test_push_telemetry_matches_a_legacy_bare_name_ignore():
167+
comment = _make_comment("SocketSecurity ignore lodash@4.17.21")
168+
_, ignore_commands = Comments.get_ignore_options({"ignore": [comment]})
169+
alert = SimpleNamespace(
170+
pkg_type="npm",
171+
pkg_name="lodash",
172+
pkg_version="4.17.21",
173+
)
174+
175+
assert _match_ignored_alerts_for_telemetry(
176+
[alert],
177+
False,
178+
ignore_commands,
179+
) == [alert]
180+
181+
164182
def _build_event(comment, ignore_all=False, ignore_commands=None, artifact_input=None, artifact_purl=None):
165183
"""Mirrors the event construction logic in socketcli.py."""
166184
from datetime import datetime, timezone

0 commit comments

Comments
 (0)