<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marian Ganišin</title>
    <description>The latest articles on DEV Community by Marian Ganišin (@mangan37).</description>
    <link>https://dev.to/mangan37</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1914239%2F61982b0b-a3a8-4563-b285-884a342141c8.png</url>
      <title>DEV Community: Marian Ganišin</title>
      <link>https://dev.to/mangan37</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mangan37"/>
    <language>en</language>
    <item>
      <title>Securing Testing Secrets with pytest-mask-secrets</title>
      <dc:creator>Marian Ganišin</dc:creator>
      <pubDate>Sun, 11 Aug 2024 16:26:51 +0000</pubDate>
      <link>https://dev.to/mangan37/securing-testing-secrets-with-pytest-mask-secrets-1ohm</link>
      <guid>https://dev.to/mangan37/securing-testing-secrets-with-pytest-mask-secrets-1ohm</guid>
      <description>&lt;p&gt;Keeping sensitive data secure and private is a top priority in software development. Application logs, one of the common leakage vectors, are carefully guarded to prevent the presence of secrets. The same concern and risk apply also to testing logs, which can reveal passwords or access tokens. Tools running CI workflows usually provide mechanisms to mask sensitive data in logs with little to no effort. While this is very convenient, efficient and easy to use, in certain situations, this may not be sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CI Workflow Masking Alone Might Not Be Enough
&lt;/h2&gt;

&lt;p&gt;For instance, GitHub Actions does a good job of handling secrets. Any secret defined within the workflow is automatically masked from the captured output, which works like a charm. However, like any CI system, it has its limitations. If the output report takes a different path—such as being saved to a file, junit is generated or sent to a remote log store—GitHub Actions doesn't have the ability to inspect the content and mask the secrets.&lt;/p&gt;

&lt;p&gt;Moreover, test won't always run within a CI workflow, and even then, secrets may still need to be hidden. Imagine you're running tests locally and share a log to discuss an issue. Without realizing it, you include a URL with your access token.&lt;/p&gt;

&lt;p&gt;Therefore, having a mechanism to handle sensitive data in test logs is essential at all levels. The best approach is to implement this directly at the test level or within the test framework itself. This ensures that secrets are not leaked from the primary source, preventing them from being passed up through the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Protection at the Right Level
&lt;/h2&gt;

&lt;p&gt;Maintaining the masking of secrets directly in tests can be relatively costly and error-prone, and often feels like a losing battle. For example, imagine you need to design a URL with a token as a parameter. This URL must be rendered differently for use in a request compared to its presence in the log.&lt;/p&gt;

&lt;p&gt;In contrast, intercepting the report generation within the test framework provides an ideal opportunity to hook into the process and modify the records to eliminate sensitive data. This approach is transparent to the tests, requires no modifications to the test code, and functions just like the secret-masking feature in a CI workflows—simply run it and forget about managing the secrets. It automates the process, ensuring that sensitive data is protected without adding extra complexity to the test setup.&lt;/p&gt;

&lt;p&gt;This is exactly what &lt;a href="https://github.com/mganisin/pytest-mask-secrets" rel="noopener noreferrer"&gt;pytest-mask-secrets&lt;/a&gt; does, obviously when pytest is in use for test execution. Among its many features, pytest offers a rich and flexible plugin system. For this purpose, it allows you to hook into the process just before any log is generated, at a point when all the data has already been collected. This makes it easy to search for and remove sensitive values from the records before they are outputted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It to the Test: A Practical Demo
&lt;/h2&gt;

&lt;p&gt;To illustrate how this works, a simple example will be most effective. Below is a trivial test that may not represent a real-world testing scenario but serves the purpose of demonstrating &lt;code&gt;pytest-mask-secrets&lt;/code&gt; quite well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_password_length&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tested password: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, there’s an assertion that has the potential to fail (and it will), along with a log message that includes a secret. Yes, it might seem silly to include a secret in log, but consider a scenario where you have a URL with a token as a parameter, and detailed debug logging is enabled. In such cases, libraries like &lt;code&gt;requests&lt;/code&gt; might inadvertently log the secret in this way.&lt;/p&gt;

&lt;p&gt;Now for the testing. First, set the secret needed for testing purposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ export PASSWORD="TOP-SECRET"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, run the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ pytest --log-level=info test.py 
============================= test session starts ==============================
platform linux -- Python 3.12.4, pytest-8.3.2, pluggy-1.5.0
rootdir: /tmp/tmp.AvZtz7nHZS
collected 1 item                                                               

test.py F                                                                [100%]

=================================== FAILURES ===================================
_____________________________ test_password_length _____________________________

    def test_password_length():
        password = os.environ["PASSWORD"]
        logging.info("Tested password: %s", password)
&amp;gt;       assert len(password) &amp;gt; 18
E       AssertionError: assert 10 &amp;gt; 18
E        +  where 10 = len('TOP-SECRET')

test.py:8: AssertionError
------------------------------ Captured log call -------------------------------
INFO     root:test.py:7 Tested password: TOP-SECRET
=========================== short test summary info ============================
FAILED test.py::test_password_length - AssertionError: assert 10 &amp;gt; 18
============================== 1 failed in 0.03s ===============================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, the secret value appears twice in the output: once in the captured log message and again in the failed assertion. &lt;/p&gt;

&lt;p&gt;But what if &lt;code&gt;pytest-mask-secrets&lt;/code&gt; is installed?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ pip install pytest-mask-secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And configured accordingly. It needs to know list of environment variables that hold the secrets. This is done by setting &lt;code&gt;MASK_SECRETS&lt;/code&gt; variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ export MASK_SECRETS=PASSWORD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, rerun the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ pytest --log-level=info test.py 
============================= test session starts ==============================
platform linux -- Python 3.12.4, pytest-8.3.2, pluggy-1.5.0
rootdir: /tmp/tmp.AvZtz7nHZS
plugins: mask-secrets-1.2.0
collected 1 item                                                               

test.py F                                                                [100%]

=================================== FAILURES ===================================
_____________________________ test_password_length _____________________________

    def test_password_length():
        password = os.environ["PASSWORD"]
        logging.info("Tested password: %s", password)
&amp;gt;       assert len(password) &amp;gt; 18
E       AssertionError: assert 10 &amp;gt; 18
E        +  where 10 = len('*****')

test.py:8: AssertionError
------------------------------ Captured log call -------------------------------
INFO     root:test.py:7 Tested password: *****
=========================== short test summary info ============================
FAILED test.py::test_password_length - AssertionError: assert 10 &amp;gt; 18
============================== 1 failed in 0.02s ===============================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, instead of the secret value, asterisks appear wherever the secret would have been printed. The job is done, and the test report is now free of sensitive data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;From the example, it might seem that &lt;code&gt;pytest-mask-secrets&lt;/code&gt; doesn't do much more than what GitHub Actions already does by default, making the effort appear redundant. However, as mentioned earlier, CI worklfow execution tools only mask secrets in captured output, leaving JUnit files and other reports unaltered. Without &lt;code&gt;pytest-mask-secrets&lt;/code&gt;, sensitive data could still be exposed in these files—this applies to any report generated by pytest. On the other hand, &lt;code&gt;pytest-mask-secrets&lt;/code&gt; does not mask direct output when the &lt;code&gt;log_cli&lt;/code&gt; option is used, so the masking features of CI workflows are still useful. It’s often best to use both tools in conjunction to ensure the protection of sensitive data.&lt;/p&gt;

&lt;p&gt;This is it. Thank you for taking the time to read this post. I hope it provided valuable insights into using &lt;code&gt;pytest-mask-secrets&lt;/code&gt; to enhance the security of your testing process.&lt;/p&gt;

&lt;p&gt;Happy Testing!&lt;/p&gt;

</description>
      <category>testing</category>
      <category>python</category>
      <category>pytest</category>
    </item>
  </channel>
</rss>
