Skip to content

fix(diffStix): write the changelog markdown as UTF-8 - #246

Open
ppcvote wants to merge 1 commit into
mitre-attack:mainfrom
ppcvote:fix/changelog-markdown-utf8
Open

fix(diffStix): write the changelog markdown as UTF-8#246
ppcvote wants to merge 1 commit into
mitre-attack:mainfrom
ppcvote:fix/changelog-markdown-utf8

Conversation

@ppcvote

@ppcvote ppcvote commented Aug 13, 2026

Copy link
Copy Markdown

What breaks

get_new_changelog_md opens the markdown file without an encoding, so CPython falls back to the locale codec. ATT&CK changelog text is not ASCII, so the write raises on any machine whose locale encoding is not UTF-8:

UnicodeEncodeError: 'cp950' codec can't encode character '\u041e' in position 238
  changelog_helper.py:2433 in get_new_changelog_md

Reproduced on Windows with a cp950 console. cp1252, the default on en-US Windows, fails the same way on Cyrillic. Linux and macOS default to UTF-8, which is why CI has never seen it.

The blast radius is larger than the one file. That write sits at the top of the output block in get_new_changelog_md, so the exception also skips the HTML writer, the detailed HTML writer, the layer writers and the JSON writer. A run ends with an empty markdown file and none of the other artifacts.

This looks like an oversight rather than a decision: the sibling writers in the same module already pass the encoding explicitly, at line 1849 and line 1955.

The change

One argument at changelog_helper.py:2432:

-        with open(markdown_file, "w") as file:
+        with open(markdown_file, "w", encoding="utf-8") as file:

The other bare opens in this module are deliberately left alone. Lines 1861, 1866, 1871 and 2465 also open without an encoding, but they write through json.dump, whose ensure_ascii default keeps the payload ASCII, so they do not raise today. Happy to include them if you would rather the module be clean under -X warn_default_encoding, but they are a different question from this bug.

Three read_text() calls in the tests get the matching encoding. Before this change the round trip was symmetric by accident, locale-encoded at both ends. Once the writer is UTF-8, a reader that does not say so decodes with the locale codec, and file_content == markdown_result compares a mis-decoded string against the original. On my machine that turned two passing tests red, which is how I found them; on Linux CI they would have stayed green and the assertion would have quietly stopped meaning anything.

About the test

It drives the writer in a subprocess under -X warn_default_encoding -W error::EncodingWarning rather than asserting on a round trip.

A round trip is the obvious test and it is useless here: it passes on an unfixed tree wherever the locale encoding is already UTF-8, so it would have gone green on your CI both before and after the fix. I checked that before writing this one. Under PEP 597 any open() that relies on the locale codec becomes an error, so the test fails on every platform if the encoding argument goes missing again, and the traceback names the exact line.

The driver passes layers=[] and json_file=None so only the markdown writer is exercised, keeping the assertion pointed at one thing.

Verification

  • New test: fails on unmodified main, passes with the fix. Confirmed in both directions by stashing the change and re-running.
  • tests/changelog: 7 failures before this change and the same 7 after, none of them new. They are all in cli/ and are pre-existing Windows environment assumptions, for example a pytest.raises(PermissionError) that Windows does not raise.
  • ruff check and ruff format --check clean on all four touched files.

`get_new_changelog_md` opened the markdown file without an encoding, so
CPython fell back to the locale codec. ATT&CK changelog text is not ASCII, so
on any machine whose locale encoding is not UTF-8 the write raises:

    UnicodeEncodeError: 'cp950' codec can't encode character '\u041e'
      changelog_helper.py:2433 in get_new_changelog_md

Reproduced on Windows with a cp950 console; cp1252, the default on en-US
Windows, fails the same way on Cyrillic. The write sits at the top of the
output block, so the exception also skips the HTML, detailed HTML, layer and
JSON writers, leaving an empty markdown file and no other artifacts.

This is an oversight rather than a policy: the sibling writers in the same
module at lines 1849 and 1955 both pass `encoding="utf-8"` already.

The remaining bare opens in this module (1861, 1866, 1871, 2465) go through
`json.dump`, whose `ensure_ascii` default keeps their payload ASCII, so they
do not raise today and are left alone.

The three test-side `read_text()` calls are given the matching encoding.
Before this change the round trip was symmetric by accident, locale-encoded on
both ends; once the writer is UTF-8 the readers have to say so, or
`file_content == markdown_result` compares a mis-decoded string.

The new test drives the writer in a subprocess under
`-X warn_default_encoding -W error::EncodingWarning`, rather than asserting on
a round trip. A round trip passes on an unfixed tree wherever the locale
encoding is already UTF-8, so on Linux CI it would have gone green either way
and pinned nothing. Under PEP 597 it fails on every platform if the encoding
argument goes missing again.

Verified: fails before, passes after. `tests/changelog` has the same 7 failures
before and after this change, all in `cli/` and all pre-existing Windows
environment assumptions such as `pytest.raises(PermissionError)`.
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant