diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a00ee8dc..d334bf15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] - python: ['3.6', '3.7', '3.8', '3.9'] + os: [windows-latest] + python: ['3.8', '3.9'] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 diff --git a/testing/mocks.py b/testing/mocks.py index b97a38cb..43f93cf7 100644 --- a/testing/mocks.py +++ b/testing/mocks.py @@ -1,5 +1,7 @@ """This is a collection of utility functions for easier, DRY testing.""" import io +import os +import tempfile from collections import defaultdict from contextlib import contextmanager from types import ModuleType @@ -88,3 +90,17 @@ def disable_gibberish_filter() -> Iterator[None]: return_value=False, ): yield + + +@contextmanager +def mock_baseline_file(mode: str = 'w+b') -> Iterator[IO[Any]]: + """ + Used to create a mock temporary baseline file. To avoid operating system differences, + specifically Linux vs. Windows on how "NamedTemporaryFile" operate, we will perform + the creation and cleanup of the temporary file here. + """ + with tempfile.NamedTemporaryFile(mode=mode, delete=False) as f: + yield f + + f.close() + os.unlink(f.name) diff --git a/tests/audit/analytics_test.py b/tests/audit/analytics_test.py index 156a1354..1f696a28 100644 --- a/tests/audit/analytics_test.py +++ b/tests/audit/analytics_test.py @@ -1,7 +1,6 @@ import json import random import string -import tempfile from contextlib import contextmanager import pytest @@ -11,6 +10,7 @@ from detect_secrets.main import main from detect_secrets.plugins.basic_auth import BasicAuthDetector from testing.factories import potential_secret_factory as original_potential_secret_factory +from testing.mocks import mock_baseline_file def potential_secret_factory(**kwargs): @@ -59,7 +59,7 @@ def test_basic_statistics_json(printer): def test_no_divide_by_zero(secret): secrets = SecretsCollection() secrets['file'].add(secret) - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) @@ -84,7 +84,7 @@ def labelled_secrets(): potential_secret_factory(is_secret=False), } - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) diff --git a/tests/audit/audit_test.py b/tests/audit/audit_test.py index b71d5d03..e7c8d761 100644 --- a/tests/audit/audit_test.py +++ b/tests/audit/audit_test.py @@ -1,6 +1,5 @@ import json import random -import tempfile from typing import List from typing import Optional from unittest import mock @@ -12,6 +11,7 @@ from detect_secrets.main import main from detect_secrets.settings import transient_settings from testing.factories import potential_secret_factory +from testing.mocks import mock_baseline_file def test_nothing_to_audit(printer): @@ -166,7 +166,7 @@ def run_logic( :param input: if provided, will automatically quit at the end of input string. otherwise, will assert that no user input is requested. """ - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) diff --git a/tests/audit/compare_test.py b/tests/audit/compare_test.py index 893ac6a4..230b9e9b 100644 --- a/tests/audit/compare_test.py +++ b/tests/audit/compare_test.py @@ -1,5 +1,4 @@ import re -import tempfile from contextlib import contextmanager from unittest import mock @@ -11,6 +10,7 @@ from detect_secrets.main import main from detect_secrets.plugins.basic_auth import BasicAuthDetector from testing.factories import potential_secret_factory as original_potential_secret_factory +from testing.mocks import mock_baseline_file def potential_secret_factory(secret: str, **kwargs): @@ -139,7 +139,7 @@ def test_fails_when_no_line_number(printer): def run_logic(secretsA: SecretsCollection, secretsB: SecretsCollection): - with tempfile.NamedTemporaryFile() as f, tempfile.NamedTemporaryFile() as g: + with mock_baseline_file() as f, mock_baseline_file() as g: baseline.save_to_file(secretsA, f.name) baseline.save_to_file(secretsB, g.name) diff --git a/tests/audit/report_test.py b/tests/audit/report_test.py index d544d21e..a7a08259 100644 --- a/tests/audit/report_test.py +++ b/tests/audit/report_test.py @@ -1,3 +1,4 @@ +import os import random import string import tempfile @@ -14,6 +15,7 @@ from detect_secrets.plugins.basic_auth import BasicAuthDetector from detect_secrets.plugins.jwt import JwtTokenDetector from detect_secrets.settings import transient_settings +from testing.mocks import mock_baseline_file url_format = 'http://username:{}@www.example.com/auth' @@ -166,11 +168,14 @@ def count_results(data): @contextmanager def create_file_with_content(content): - with tempfile.NamedTemporaryFile() as f: + with tempfile.NamedTemporaryFile(delete=False) as f: f.write(content.encode()) f.seek(0) yield f.name + f.close() + os.unlink(f.name) + @pytest.fixture def baseline_file(): @@ -187,7 +192,7 @@ def baseline_file(): with create_file_with_content(first_content) as first_file, \ create_file_with_content(second_content) as second_file, \ - tempfile.NamedTemporaryFile() as baseline_file, \ + mock_baseline_file() as baseline_file, \ transient_settings({ 'plugins_used': [ {'name': 'BasicAuthDetector'}, diff --git a/tests/core/baseline_test.py b/tests/core/baseline_test.py index 7fbefbac..e7e568a3 100644 --- a/tests/core/baseline_test.py +++ b/tests/core/baseline_test.py @@ -1,6 +1,8 @@ import json +import os import subprocess import tempfile +from pathlib import Path from unittest import mock import pytest @@ -39,8 +41,8 @@ def test_basic_usage(path): secrets = baseline.create(path) assert len(secrets.data.keys()) == 2 - assert len(secrets['test_data/files/file_with_secrets.py']) == 1 - assert len(secrets['test_data/files/tmp/file_with_secrets.py']) == 2 + assert len(secrets[str(Path('test_data/files/file_with_secrets.py'))]) == 1 + assert len(secrets[str(Path('test_data/files/tmp/file_with_secrets.py'))]) == 2 @staticmethod def test_error_when_getting_git_tracked_files(): @@ -69,7 +71,9 @@ def test_no_files_in_git_repo(): @staticmethod def test_scan_all_files(): - with tempfile.NamedTemporaryFile(dir='test_data/files/tmp', suffix='.py') as f: + with tempfile.NamedTemporaryFile( + dir='test_data/files/tmp', suffix='.py', delete=False, + ) as f: f.write(b'"2b00042f7481c7b056c4b410d28f33cf"') f.seek(0) @@ -79,6 +83,9 @@ def test_scan_all_files(): secrets = baseline.create('test_data/files/tmp', should_scan_all_files=True) assert get_relative_path_if_in_cwd(f.name) in secrets.data + f.close() + os.unlink(f.name) + def test_load_and_output(): with open('.secrets.baseline') as f: diff --git a/tests/core/scan_test.py b/tests/core/scan_test.py index 279f8e4e..2316d898 100644 --- a/tests/core/scan_test.py +++ b/tests/core/scan_test.py @@ -1,6 +1,7 @@ import os import tempfile import textwrap +from pathlib import Path import pytest @@ -53,11 +54,11 @@ def test_handles_each_path_separately(non_tracked_file): @staticmethod def test_handles_multiple_directories(): - directories = ['test_data/short_files', 'test_data/files'] + directories = [Path('test_data/short_files'), Path('test_data/files')] results = list(scan.get_files_to_scan(*directories)) for prefix in directories: - assert len(list(filter(lambda x: x.startswith(prefix), results))) > 1 + assert len(list(filter(lambda x: x.startswith(str(prefix)), results))) > 1 @staticmethod @pytest.fixture(autouse=True, scope='class') @@ -74,7 +75,7 @@ def non_tracked_file(): class TestScanFile: @staticmethod def test_handles_broken_yaml_gracefully(): - with tempfile.NamedTemporaryFile(suffix='.yaml') as f: + with tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) as f: f.write( textwrap.dedent(""" metadata: @@ -85,16 +86,22 @@ def test_handles_broken_yaml_gracefully(): assert not list(scan.scan_file(f.name)) + f.close() + os.unlink(f.name) + @staticmethod def test_handles_binary_files_gracefully(): # NOTE: This suffix needs to be something that isn't in the known file types, as determined # by `detect_secrets.util.filetype.determine_file_type`. - with tempfile.NamedTemporaryFile(suffix='.woff2') as f: + with tempfile.NamedTemporaryFile(suffix='.woff2', delete=False) as f: f.write(b'\x86') f.seek(0) assert not list(scan.scan_file(f.name)) + f.close() + os.unlink(f.name) + @pytest.fixture(autouse=True) def configure_plugins(): diff --git a/tests/core/usage/baseline_usage_test.py b/tests/core/usage/baseline_usage_test.py index 39db8a76..800f5559 100644 --- a/tests/core/usage/baseline_usage_test.py +++ b/tests/core/usage/baseline_usage_test.py @@ -1,5 +1,4 @@ import json -import tempfile from contextlib import contextmanager import pytest @@ -7,6 +6,7 @@ from detect_secrets.core.plugins.util import get_mapping_from_secret_type_to_class from detect_secrets.core.usage import ParserBuilder from detect_secrets.settings import get_settings +from testing.mocks import mock_baseline_file @pytest.fixture @@ -60,7 +60,7 @@ def test_success(parser): @contextmanager def _mock_file(content: str): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: f.write(content.encode()) f.seek(0) diff --git a/tests/core/usage/filters_usage_test.py b/tests/core/usage/filters_usage_test.py index f7e0a4c2..94111df0 100644 --- a/tests/core/usage/filters_usage_test.py +++ b/tests/core/usage/filters_usage_test.py @@ -1,4 +1,3 @@ -import tempfile import uuid import pytest @@ -10,11 +9,12 @@ from detect_secrets.settings import default_settings from detect_secrets.settings import get_settings from detect_secrets.settings import transient_settings +from testing.mocks import mock_baseline_file def test_no_verify_overrides_baseline_settings(parser): secrets = SecretsCollection() - with tempfile.NamedTemporaryFile() as f, transient_settings({ + with mock_baseline_file() as f, transient_settings({ 'filters_used': [{ 'path': 'detect_secrets.filters.common.is_ignored_due_to_verification_policies', 'min_level': VerifiedResult.UNVERIFIED.value, @@ -30,7 +30,7 @@ def test_no_verify_overrides_baseline_settings(parser): def test_only_verified_overrides_baseline_settings(parser): secrets = SecretsCollection() - with tempfile.NamedTemporaryFile() as f, transient_settings({ + with mock_baseline_file() as f, transient_settings({ 'filters_used': [{ 'path': 'detect_secrets.filters.common.is_ignored_due_to_verification_policies', 'min_level': VerifiedResult.UNVERIFIED.value, @@ -134,7 +134,7 @@ def test_module_failure(parser, filepath): def test_disable_filter(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: f.write(f'secret = "{uuid.uuid4()}"'.encode()) # First, make sure that we actually catch it. diff --git a/tests/core/usage/plugins_usage_test.py b/tests/core/usage/plugins_usage_test.py index f1e12dbc..e8a486d8 100644 --- a/tests/core/usage/plugins_usage_test.py +++ b/tests/core/usage/plugins_usage_test.py @@ -1,6 +1,5 @@ import json import os -import tempfile import pytest @@ -9,6 +8,7 @@ from detect_secrets.core.secrets_collection import SecretsCollection from detect_secrets.core.usage import ParserBuilder from detect_secrets.settings import get_settings +from testing.mocks import mock_baseline_file @pytest.fixture @@ -48,7 +48,7 @@ def test_failure(parser, flag, value): @staticmethod def test_precedence_with_only_baseline(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: f.write( json.dumps({ 'version': '0.0.1', @@ -69,7 +69,7 @@ def test_precedence_with_only_baseline(parser): @staticmethod def test_precedence_with_baseline_and_explicit_value(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: f.write( json.dumps({ 'version': '0.0.1', @@ -115,7 +115,7 @@ def test_invalid_classname(parser): @staticmethod def test_precedence_with_baseline(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: f.write( json.dumps({ 'version': '0.0.1', @@ -148,7 +148,7 @@ def test_success(parser): # Ensure it serializes accordingly. parser.parse_args(['-p', 'testing/plugins.py']) - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file(SecretsCollection(), f.name) f.seek(0) diff --git a/tests/core/usage/scan_usage_test.py b/tests/core/usage/scan_usage_test.py index 1933db85..88894040 100644 --- a/tests/core/usage/scan_usage_test.py +++ b/tests/core/usage/scan_usage_test.py @@ -1,5 +1,4 @@ import json -import tempfile import pytest @@ -7,6 +6,7 @@ from detect_secrets.core.plugins.util import get_mapping_from_secret_type_to_class from detect_secrets.core.usage import ParserBuilder from detect_secrets.settings import get_settings +from testing.mocks import mock_baseline_file @pytest.fixture @@ -15,7 +15,7 @@ def parser(): def test_force_use_all_plugins(parser): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: f.write( json.dumps({ 'version': '0.0.1', diff --git a/tests/filters/wordlist_filter_test.py b/tests/filters/wordlist_filter_test.py index a8fa9177..71073ff4 100644 --- a/tests/filters/wordlist_filter_test.py +++ b/tests/filters/wordlist_filter_test.py @@ -1,6 +1,9 @@ +from pathlib import Path + import pytest from detect_secrets import filters +from detect_secrets.filters.util import compute_file_hash from detect_secrets.settings import get_settings from detect_secrets.settings import transient_settings @@ -17,6 +20,9 @@ def initialize_automaton(): @staticmethod def test_success(): + # Compute file_hash manually due to file path operating system differences + file_hash = compute_file_hash(Path('test_data/word_list.txt')) + # case-insensitivity assert filters.wordlist.should_exclude_secret('testPass') is True @@ -25,9 +31,7 @@ def test_success(): assert get_settings().filters['detect_secrets.filters.wordlist.should_exclude_secret'] == { 'min_length': 8, - - # Manually computed with `sha1sum test_data/word_list.txt` - 'file_hash': '116598304e5b33667e651025bcfed6b9a99484c7', + 'file_hash': file_hash, 'file_name': 'test_data/word_list.txt', } diff --git a/tests/main_test.py b/tests/main_test.py index 4a2bb98e..f65ef146 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -4,6 +4,7 @@ import tempfile from contextlib import contextmanager from contextlib import redirect_stdout +from pathlib import Path from unittest import mock from detect_secrets import main as main_module @@ -12,6 +13,7 @@ from detect_secrets.main import scan_adhoc_string from detect_secrets.settings import transient_settings from testing.mocks import disable_gibberish_filter +from testing.mocks import mock_baseline_file from testing.mocks import mock_printer @@ -39,7 +41,7 @@ def test_saves_to_baseline(): secrets = SecretsCollection() old_secrets = baseline.format_for_output(secrets) - with mock_printer(main_module) as printer, tempfile.NamedTemporaryFile() as f: + with mock_printer(main_module) as printer, mock_baseline_file() as f: baseline.save_to_file(old_secrets, f.name) f.seek(0) @@ -83,26 +85,28 @@ def test_basic(): @staticmethod def test_restores_line_numbers(): - with tempfile.NamedTemporaryFile('w+') as f: - with redirect_stdout(f): - main_module.main(['scan', '--slim', 'test_data/config.env']) + with mock_baseline_file('w+') as f, redirect_stdout(f): + file_a = str(Path('test_data/config.env')) + file_b = str(Path('test_data/config.md')) + + main_module.main(['scan', '--slim', file_a]) f.seek(0) main_module.main([ 'scan', - '--slim', 'test_data/config.md', 'test_data/config.env', + '--slim', file_a, file_b, '--baseline', f.name, ]) f.seek(0) secrets = baseline.load(baseline.load_from_file(f.name)) - # Make sure both old and new files exist - assert secrets.files == {'test_data/config.env', 'test_data/config.md'} + # Make sure both old and new files exist + assert secrets.files == {file_a, file_b} - # Make sure they both have line numbers - assert list(secrets['test_data/config.env'])[0].line_number - assert list(secrets['test_data/config.md'])[0].line_number + # Make sure they both have line numbers + assert list(secrets[file_a])[0].line_number + assert list(secrets[file_b])[0].line_number class TestScanString: @@ -183,7 +187,7 @@ def test_basic(mock_log): for item in output['filters_used'] } - with tempfile.NamedTemporaryFile() as f, mock.patch( + with mock_baseline_file() as f, mock.patch( 'detect_secrets.audit.io.get_user_decision', return_value='s', ): diff --git a/tests/plugins/keyword_test.py b/tests/plugins/keyword_test.py index ce8b0748..114572a0 100644 --- a/tests/plugins/keyword_test.py +++ b/tests/plugins/keyword_test.py @@ -44,7 +44,7 @@ ('password: ${link}', None), # Has a ${ followed by a } ('some_key = "real_secret"', None), # We cannot make 'key' a Keyword, too noisy) ('private_key "hopenobodyfindsthisone\';', None), # Double-quote does not match single-quote) - (LONG_LINE, None), # Long line test + # (LONG_LINE, None), # Long line test ] GOLANG_TEST_CASES = [ @@ -83,7 +83,7 @@ ('password := "somefakekey"', None), # 'fake' in the secret ('some_key = "real_secret"', None), # We cannot make 'key' a Keyword, too noisy) ('private_key "hopenobodyfindsthisone\';', None), # Double-quote does not match single-quote) - (LONG_LINE, None), # Long line test + # (LONG_LINE, None), # Long line test ] COMMON_C_TEST_CASES = [ @@ -106,7 +106,7 @@ ('password = "somefakekey"', None), # 'fake' in the secret ('password[] = ${link}', None), # Has a ${ followed by a } ('some_key = "real_secret"', None), # We cannot make 'key' a Keyword, too noisy) - (LONG_LINE, None), # Long line test + # (LONG_LINE, None), # Long line test ] C_PLUS_PLUS_TEST_CASES = [ @@ -147,6 +147,10 @@ ('password: ${link}', None), # Has a ${ followed by a } ('some_key = "real_secret"', None), # We cannot make 'key' a Keyword, too noisy) ('private_key "hopenobodyfindsthisone\';', None), # Double-quote does not match single-quote) + # (LONG_LINE, None), # Long line test +] + +LONG_TEST_CASES = [ (LONG_LINE, None), # Long line test ] @@ -194,6 +198,42 @@ def test_keyword(file_extension, line, expected_secret): assert not secrets +@pytest.mark.parametrize( + 'file_extension, line, expected_secret', + ( + parse_test_cases([ + ('conf', LONG_TEST_CASES), + ('go', LONG_TEST_CASES), + ('m', LONG_TEST_CASES), + ('c', LONG_TEST_CASES), + ('cs', LONG_TEST_CASES), + ('cls', LONG_TEST_CASES), + ('java', LONG_TEST_CASES), + ('py', LONG_TEST_CASES), + ('pyi', LONG_TEST_CASES), + ('js', LONG_TEST_CASES), + ('swift', LONG_TEST_CASES), + ('tf', LONG_TEST_CASES), + (None, LONG_TEST_CASES), + ]) + ), +) +def test_long_keyword(file_extension, line, expected_secret): + if not file_extension: + secrets = list(scan_line(line)) + else: + secrets = list( + KeywordDetector(keyword_exclude='.*fake.*').analyze_line( + filename='mock_filename.{}'.format(file_extension), + line=line, + ), + ) + if expected_secret: + assert secrets[0].secret_value == expected_secret + else: + assert not secrets + + @pytest.fixture(autouse=True) def use_keyword_detector(): with transient_settings({ diff --git a/tests/plugins/private_key_test.py b/tests/plugins/private_key_test.py index 533ead34..e4e268e8 100644 --- a/tests/plugins/private_key_test.py +++ b/tests/plugins/private_key_test.py @@ -1,9 +1,8 @@ -import tempfile - import pytest from detect_secrets.core.secrets_collection import SecretsCollection from detect_secrets.settings import transient_settings +from testing.mocks import mock_baseline_file @pytest.mark.parametrize( @@ -22,7 +21,7 @@ ], ) def test_basic(file_content): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: f.write(file_content.encode()) f.seek(0) diff --git a/tests/pre_commit_hook_test.py b/tests/pre_commit_hook_test.py index 4c44fd7f..ecaa15aa 100644 --- a/tests/pre_commit_hook_test.py +++ b/tests/pre_commit_hook_test.py @@ -1,5 +1,4 @@ import json -import tempfile from contextlib import contextmanager from functools import partial from typing import List @@ -12,6 +11,7 @@ from detect_secrets.pre_commit_hook import main from detect_secrets.settings import transient_settings from testing.mocks import disable_gibberish_filter +from testing.mocks import mock_baseline_file @pytest.fixture(autouse=True) @@ -54,7 +54,7 @@ def test_baseline_filters_out_known_secrets(): assert secrets with disable_gibberish_filter(): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) @@ -68,7 +68,7 @@ def test_baseline_filters_out_known_secrets(): # Remove one arbitrary secret, so that it won't be the full set. secrets.data['test_data/each_secret.py'].pop() - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file(secrets, f.name) f.seek(0) @@ -128,7 +128,7 @@ def get_baseline_file(self, formatter=baseline.format_for_output): secrets = SecretsCollection() secrets.scan_file(self.FILENAME) - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: with mock.patch('detect_secrets.core.baseline.VERSION', '0.0.1'): data = formatter(secrets) @@ -143,7 +143,7 @@ class TestLineNumberChanges: FILENAME = 'test_data/files/file_with_secrets.py' def test_modifies_baseline(self, modified_baseline): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file(modified_baseline, f.name) assert_commit_blocked_with_diff_exit_code([ @@ -153,7 +153,7 @@ def test_modifies_baseline(self, modified_baseline): ]) def test_does_not_modify_slim_baseline(self, modified_baseline): - with tempfile.NamedTemporaryFile() as f: + with mock_baseline_file() as f: baseline.save_to_file( baseline.format_for_output(modified_baseline, is_slim_mode=True), f.name,