Skip to content

chore(release): 3.4.0 - #120

Merged
lelia merged 11 commits into
mainfrom
lelia/release-3.4.0
Sep 19, 2026
Merged

lelia merged 11 commits into
mainfrom
lelia/release-3.4.0

Conversation

@lelia

@lelia lelia commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Release prep for 3.4.0, covering #119 and the already-merged #117. Version files, uv.lock and every current-release reference in README.md and docs/** are bumped, and the [Unreleased] changelog section is stamped as [3.4.0] - 2026-09-18.

Stacked on #119. This branch is cut from #119's head so the changelog entries it stamps are present. Merge #119 first; this one merges last, and the tag goes on its merge commit.

Changes

  • pyproject.toml, socket_basics/version.py, socket_basics/__init__.py, action.yml, uv.lock → 3.4.0
  • 83 current-release references synchronized across README.md and docs/** (version strings only), scripts/check_release_docs.py --check passes
  • CHANGELOG.md: [Unreleased][3.4.0] - 2026-09-18, plus a release summary and an ### Upgrade notes section covering the snippet-content change
  • CHANGELOG.md: records socketdev 3.5.0 → 3.6.0 (chore(deps): bump socketdev from 3.5.0 to 3.6.0 in the python-minor-patch group #117), which merged without an entry. Lockfile only; the >=3.5.0 floor in pyproject.toml is unchanged.

Not included

#118's tool pins. That branch is still a draft, blocked on ghcr.io/socketdev/trivy:0.74.0 being published, and maintenance upgrades are deferred to a later release. It will renumber when it comes off draft.


Note

Medium Risk
Finding codeSnippet, descriptions, and traces change for hardcoded-credential rules, which can break snippet-based baselines and alters what notifiers and facts files contain—intentionally reducing secret exposure but requiring consumer awareness on upgrade.

Overview
Release 3.4.0 ships the credential-masking work from #119 plus lockfile refresh for socketdev 3.6.0 (#117). Version constants, action.yml / GHCR image pins, uv.lock, and docs examples move from 3.3.0 → 3.4.0; CHANGELOG.md adds upgrade notes because finding payloads change on upgrade.

The functional change is a much more conservative redact_literals pipeline in socket_basics/core/utils/redaction.py: assignment binding respects annotations, operators inside literals, ;-separated statements, and trailing #////-- comments; call-shaped assignments with literal arguments stay readable while literals are still masked; multiline / prefixed / interpolated literals and unterminated quotes are handled fail-closed. Credential rules still get composed masking via redact_snippet / messages / dataflow traces, with redact rule metadata documented in the changelog.

Coverage expands with targeted unit tests (tests/test_secret_redaction.py) and a 20k-iteration fuzz harness (tests/test_secret_redaction_fuzz.py) asserting secrets never leak from generated snippets.

Reviewed by Cursor Bugbot for commit d790339. Configure here.

@lelia
lelia requested a review from a team as a code owner September 18, 2026 21:52
@lelia
lelia deployed to socket-firewall September 18, 2026 21:52 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 18, 2026 21:52 — with GitHub Actions Active

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread socket_basics/core/connector/trufflehog/__init__.py
Comment thread socket_basics/core/utils/redaction.py
@lelia
lelia deployed to socket-firewall September 18, 2026 22:00 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 18, 2026 22:01 — with GitHub Actions Active
@lelia
lelia force-pushed the lelia/release-3.4.0 branch from f9652d6 to 58f5e3c Compare September 18, 2026 22:06
@lelia
lelia deployed to socket-firewall September 18, 2026 22:06 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 18, 2026 22:06 — with GitHub Actions Active
@lelia

lelia commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread socket_basics/core/utils/redaction.py
@lelia
lelia force-pushed the lelia/release-3.4.0 branch from 58f5e3c to a23d9b0 Compare September 18, 2026 22:22
@lelia
lelia deployed to socket-firewall September 18, 2026 22:22 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 18, 2026 22:22 — with GitHub Actions Active
@lelia

lelia commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@lelia
lelia force-pushed the lelia/release-3.4.0 branch from a23d9b0 to edf77c9 Compare September 18, 2026 22:37
@lelia
lelia deployed to socket-firewall September 18, 2026 22:37 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 18, 2026 22:37 — with GitHub Actions Active
Stamps [Unreleased] as [3.4.0], bumps the version files and uv.lock, and
synchronizes the current-release references across README.md and docs/**.

Minor rather than patch: #119 changes the snippet, detailed report, dataflow
trace and description that every consumer of a finding reads, and adds the
`redact` rule-metadata key. Also records the socketdev 3.5.0 -> 3.6.0 lockfile
bump from #117, which merged without a changelog entry.
@lelia
lelia force-pushed the lelia/release-3.4.0 branch from edf77c9 to 126754f Compare September 18, 2026 22:41
@lelia
lelia deployed to socket-firewall September 18, 2026 22:41 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 18, 2026 22:41 — with GitHub Actions Active
@lelia

lelia commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

The rule that fragment names matches password *handling* -- assigning request
input to a password field, or comparing against one -- so its match is an
expression rather than a literal. Running the literal pass on it reduced
`user.password = request.form.get('password')` to a row of asterisks, which is
the rule's main pattern and leaves nothing to act on. It belongs with
`hardcoded-ip` and the password-policy rules, which the same comment already
excludes for the same reason. A comparison against a hardcoded value is the one
shape it covers that carries a credential, and that is what the `hardcoded-*`
rules are for.

Also folds a duplicated TestRedactMessage class into one. The second definition
shadowed the first, so two message tests never ran.
@lelia
lelia deployed to socket-firewall September 18, 2026 22:52 — with GitHub Actions Active
@lelia

lelia commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@lelia
lelia deployed to socket-firewall September 18, 2026 22:52 — with GitHub Actions Active
A literal can open on one line and close on another. Spans were computed per
line, so a marker on a literal's second line read as a comment and the rest of
that line was starred -- dropping the closing quote, after which the literal
pass no longer matched and the opening line's value survived.

Spans are now measured once over the whole snippet and consulted by absolute
position. Two things fall out of that.

A snippet is a slice of a file, so a literal can also never close. The quoted
value branch deferred to the literal pass, which never matches an unterminated
literal, so `password = "hunter2` was left untouched. It now defers only when
the quote opens a span the pass can find, and masks the value whole otherwise.

A multi-line literal body was measured as one value, so the head-and-tail
reveal exposed the start of its first line. Each line of such a body is now
masked whole, with the line breaks kept so the snippet still shows where the
literal begins and ends.
@lelia

lelia commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@lelia
lelia deployed to socket-firewall September 18, 2026 23:46 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 18, 2026 23:46 — with GitHub Actions Active

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Prefixed multiline strings leak credentials
    • Prefixed openers such as r""" are now recognized as reconstructable literals, so the first-line quotes are kept and the later-line body is masked instead of leaking.

Create PR

Or push these changes by commenting:

@cursor push a9e5624282
Preview (a9e5624282)
diff --git a/socket_basics/core/utils/redaction.py b/socket_basics/core/utils/redaction.py
--- a/socket_basics/core/utils/redaction.py
+++ b/socket_basics/core/utils/redaction.py
@@ -146,6 +146,11 @@
 # such as ``# see get_secret()`` disable masking for the value in front of it.
 _CALL_EXPRESSION = re.compile(r'^[\w.\[\]]+\s*\(')
 
+# Optional prefixes on a quoted literal. ``r"""`` / ``f"..."`` / ``b'...'``
+# still open a value the literal pass can find; the quote, not the prefix,
+# is what that pass matches.
+_STRING_PREFIX = re.compile(r'^[bBfFrRuU]*')
+
 # Rule-name fragments whose finding *is* the credential. ``hardcoded-ip`` and
 # the password-policy rules deliberately do not appear: their snippets are
 # logic, and masking them would remove the reason the finding was raised.
@@ -252,8 +257,16 @@
     # nothing to act on. A quoted value is the literal pass's job -- but only
     # where the quote opens a literal that pass can find. A snippet cut mid
     # string has an opening quote and no closing one, so nothing matches and
-    # the value would survive untouched.
-    opens_literal = value.startswith(('"', "'", '`')) and in_literal(offset + len(head))
+    # the value would survive untouched. A prefix such as ``r`` or ``f`` is
+    # not the quote, but the quote after it still is: starring the opener
+    # would drop those quotes, later lines would stay marked in-literal, and
+    # the body would survive the literal pass.
+    prefix_len = _STRING_PREFIX.match(value).end()
+    opens_literal = (
+        prefix_len < len(value)
+        and value[prefix_len] in '"\'`'
+        and in_literal(offset + len(head) + prefix_len)
+    )
     if opens_literal or _CALL_EXPRESSION.match(value):
         return code
 
@@ -292,10 +305,10 @@
     finding actionable, while the value does not.
 
     Where the shape of the value is not recognized the whole value is masked
-    rather than guessed at, so a subscript, a ternary or a prefixed literal
-    (``f"..."``, ``r'...'``) loses more of the line than a plain assignment
-    does. That direction is deliberate: the rule ID, file and line still
-    identify the finding, and the alternative is leaving a credential in place.
+    rather than guessed at, so a subscript or a ternary loses more of the
+    line than a plain assignment does. That direction is deliberate: the
+    rule ID, file and line still identify the finding, and the alternative
+    is leaving a credential in place.
     """
     if not isinstance(text, str) or not text:
         return text if isinstance(text, str) else ''

diff --git a/tests/test_secret_redaction.py b/tests/test_secret_redaction.py
--- a/tests/test_secret_redaction.py
+++ b/tests/test_secret_redaction.py
@@ -309,7 +309,20 @@
         assert redacted.count("\n") == 2
         assert "hunter2" not in redacted
 
+    def test_a_prefixed_multiline_literal_is_masked(self):
+        # The prefix is not a quote, so whole-masking the first line would
+        # replace the opener with stars. Later lines stay marked in-literal,
+        # assignment and comment masking skip them, and the body survives.
+        for prefix in ("r", "f", "b", "rf", "fr", "br", "R", "rb"):
+            for quote in ('"', "'"):
+                snippet = (
+                    f"PASSWORD = {prefix}" + quote * 3
+                    + "\npassword=hunter2\n# hunter2\n"
+                    + quote * 3
+                )
+                assert "hunter2" not in redact_literals(snippet), snippet
 
+
 class TestCredentialRuleSelection:
     @pytest.mark.parametrize(
         "rule_id",

You can send follow-ups to the cloud agent here.

Comment thread socket_basics/core/utils/redaction.py Outdated
A prefixed opener such as r""" or f""" was not read as opening a literal, so
the opening line was starred and its quotes were removed. Later lines were
still measured against the original spans, which said they were inside a
literal, so nothing masked them and the final literal pass no longer matched.
The prefix is now part of the opener check.

That alone left a partial reveal: literal text around an interpolation inflates
the body past the reveal threshold, so f"{b}_SuperSecret123!" showed 123!. An
interpolated body is masked whole, on the same reasoning as a multi-line one --
it is a block of content, not a single opaque value.
@lelia

lelia commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@lelia
lelia deployed to socket-firewall September 19, 2026 00:01 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 19, 2026 00:01 — with GitHub Actions Active

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread socket_basics/core/utils/redaction.py
An unterminated triple-quoted value still produces a literal match: the engine
backtracks past the triple alternative and reads the first two quotes as an
empty string. That match was enough to send the value to the masking pass,
which then covered only those two quotes, so the credential stayed in the
snippet. Affects bare and prefixed openers alike.

Deferring now requires a span that starts at the quote and holds both
delimiters, which an empty match cannot satisfy. _STRING_LITERAL also gained
triple-quoted alternatives so a terminated block matches once with its real
body rather than as an empty string followed by a second literal.
@lelia

lelia commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@lelia
lelia deployed to socket-firewall September 19, 2026 00:59 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 19, 2026 00:59 — with GitHub Actions Active
…ariant

A generated-snippet sweep found three gaps the hand-written cases did not,
all of them the same thing: masking depends on knowing where string literals
start and end, and a truncated snippet can make that unknowable.

An unterminated literal is not reached by the assignment fallback when the
value sits in a comparison or a call argument, so it went to the masking pass,
which cannot match it. A quote outside every matched span now marks the rest of
the line as literal content.

An unclosed triple-quoted block does not simply fail to match -- its first two
quotes match as an empty string and the third pairs with any stray quote later,
producing one long span that hides a real assignment on a later line. An odd
count of triple delimiters now masks from the opener to the end.

A " or ' literal cannot hold a raw newline in any language these rules cover,
so a match that does is the same pairing artifact rather than a literal. Those
spans are discarded; backticks and triple quotes keep theirs.

tests/test_secret_redaction_fuzz.py generates the combinations rather than
listing them, and asserts no credential survives, none is partly revealed, and
non-credential snippets come through unchanged. 2,000,000 generated cases pass;
20,000 run in CI in about a second.
@lelia

lelia commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@lelia
lelia deployed to socket-firewall September 19, 2026 01:34 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 19, 2026 01:34 — with GitHub Actions Active

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Span filter leaks continued secrets
    • The span filter now keeps "/' matches whose newlines are backslash continuations, so the literal pass still masks secrets on the next line.

Create PR

Or push these changes by commenting:

@cursor push aa2f330ce9
Preview (aa2f330ce9)
diff --git a/socket_basics/core/utils/redaction.py b/socket_basics/core/utils/redaction.py
--- a/socket_basics/core/utils/redaction.py
+++ b/socket_basics/core/utils/redaction.py
@@ -170,6 +170,11 @@
 # Triple-quote delimiters, counted to detect a block that never closes.
 _TRIPLE_QUOTE = re.compile(r'\"\"\"|\'\'\'')
 
+# A newline that is not a backslash continuation. Distinguishes a pairing
+# artifact (an unclosed ``"`` / ``'`` that matched a stray quote later) from a
+# legitimate line-continued string, which most bundled languages allow.
+_UNESCAPED_NEWLINE = re.compile(r'(?<!\\)(?:\\\\)*\n')
+
 # Rule-name fragments whose finding *is* the credential. ``hardcoded-ip`` and
 # the password-policy rules deliberately do not appear: their snippets are
 # logic, and masking them would remove the reason the finding was raised.
@@ -355,12 +360,15 @@
     # rules cover, so a match that does is not a literal -- it is an unclosed
     # quote that paired with a stray one further down, and the span between them
     # would hide whatever it covers, including a real assignment on a later
-    # line. Backticks and triple quotes span lines legitimately and are kept.
+    # line. A backslash-escaped newline is line continuation, which those
+    # languages do allow; OpenGrep's ``extra.lines`` includes every line of
+    # that match, so the span has to stay or the continuation is never masked.
+    # Backticks and triple quotes span lines without escaping and are kept.
     spans = [
         match.span() for match in _STRING_LITERAL.finditer(text)
         if len(match.group('quote')) > 1
         or match.group('quote') == '`'
-        or '\n' not in match.group(0)
+        or not _UNESCAPED_NEWLINE.search(match.group(0))
     ]
 
     def in_literal(position: int) -> bool:

diff --git a/tests/test_secret_redaction.py b/tests/test_secret_redaction.py
--- a/tests/test_secret_redaction.py
+++ b/tests/test_secret_redaction.py
@@ -316,6 +316,20 @@
         for snippet in ('password = "hunter2\n# broken', "password = 'hunter2\n-- sql"):
             assert "hunter2" not in redact_literals(snippet), snippet
 
+    def test_a_backslash_continued_literal_is_masked(self):
+        """A ``\\`` newline is a real literal, not a pairing artifact.
+
+        Dropping the span stars the first-line value, including the opener,
+        so the masking pass cannot see the rest of the string. A credential
+        on the continuation line would then survive.
+        """
+        for snippet in (
+            'password = "\\\nhunter2"',
+            "password = 'first\\\nhunter2'",
+            'password = r"\\\nhunter2"',
+        ):
+            assert "hunter2" not in redact_literals(snippet), snippet
+
     def test_a_literal_spanning_lines_keeps_the_line_structure(self):
         redacted = redact_literals(self.TRIPLE_DOUBLE)
         assert redacted.count("\n") == 2

You can send follow-ups to the cloud agent here.

Comment thread socket_basics/core/utils/redaction.py
Bugbot found that a credential on a continuation line survived, and extending
the fuzzer to put the secret after the line break -- it had only ever put it
before -- found a second case immediately.

Both are the same thing: masking that stops at the opening line. Where a
literal opens and its end is unknowable, everything after is inside it as far
as any reader can tell, so masking now runs to the end of the snippet rather
than the end of the line. The two ways state is lost -- a quote no surviving
span covers, and an odd number of triple delimiters -- are handled together
instead of separately.

The second case was the opposite failure. Masking an unquoted value whole
destroyed the opening quote of a literal that continued past the line, so the
snippet-wide pass afterwards no longer matched and the rest of the literal was
left alone. Such a value is now masked only up to the opener, and the pass
takes the literal itself.

Both generators put the secret on either side of a line break, so the shape is
covered from here on. 1,000,000 generated cases pass.
@lelia

lelia commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@lelia
lelia deployed to socket-firewall September 19, 2026 01:45 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 19, 2026 01:45 — with GitHub Actions Active

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@lelia
lelia deployed to socket-firewall September 19, 2026 02:43 — with GitHub Actions Active
@lelia
lelia deployed to socket-firewall September 19, 2026 02:43 — with GitHub Actions Active
@lelia

lelia commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d790339. Configure here.

@lelia
lelia merged commit d8ef2e5 into main Sep 19, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants