From 9a6af28daea71e17380ef3941731d3867903866c Mon Sep 17 00:00:00 2001 From: Shane Jaroch Date: Fri, 26 Dec 2025 06:51:15 -0500 Subject: [PATCH] Fix errors w/ Windows & w/ GitHub runner. Configs. --- .github/workflows/testing.yaml | 74 +++++++++-------------- .github/workflows/windows-and-mac.yaml | 69 +++++++++++----------- Makefile | 57 ++++++++++++------ ffpass/__init__.py | 22 ++++--- requirements-dev.txt | 1 - requirements.txt | 1 - scripts/generate_mp_profile.py | 1 - setup.cfg | 81 ++++++++++++++++++++++++++ tests/__init__.py | 0 tests/test_mixed_keys_run.py | 2 - tests/test_run.py | 37 +++++++----- 11 files changed, 216 insertions(+), 129 deletions(-) create mode 100644 setup.cfg create mode 100644 tests/__init__.py diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index d00081b..1446a7e 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -1,3 +1,4 @@ +--- name: ffpass on: [push, pull_request_target] @@ -7,53 +8,30 @@ jobs: name: Test runs-on: [ubuntu-latest] steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install . - - - name: Test with pytest - run: | - pip install pytest - pip install pytest-cov - python -m pytest tests --junit-xml pytest.xml - - - name: Lint with flake8 - run: | - pip install flake8 - flake8 . --exclude='*venv,build' --ignore=E741,E501 - - - name: Upload Unit Test Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: Unit Test Results - path: pytest.xml - - - publish-test-results: - name: "Publish Unit Tests Results" - needs: test - runs-on: ubuntu-latest - if: success() || failure() - - steps: - - name: Download Artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 + - name: Set up Python + uses: actions/setup-python@v5 with: - check_name: Unit Test Results - github_token: ${{ secrets.GITHUB_TOKEN }} - files: "artifacts/Unit Test Results/pytest.xml" + python-version: "3.x" + + - name: Install dependencies + run: > + python -m pip install --upgrade pip && + python -m pip install + coveralls + -r requirements.txt + -r requirements-dev.txt + + - name: Test with pytest + run: make test + + - name: Lint with flake8 + run: make lint + + - name: Submit coverage report / coveralls + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python -m coveralls --service=github diff --git a/.github/workflows/windows-and-mac.yaml b/.github/workflows/windows-and-mac.yaml index 29f3b2f..0ede6a5 100644 --- a/.github/workflows/windows-and-mac.yaml +++ b/.github/workflows/windows-and-mac.yaml @@ -1,50 +1,49 @@ +--- name: windows-and-mac on: [push, pull_request_target] jobs: windows: - name: windows runs-on: [windows-latest] steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install . + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . - - name: Test with pytest - run: | - pip install pytest - pip install pytest-cov - python -m pytest tests --junit-xml pytest.xml + - name: Test with pytest + run: | + pip install pytest + pip install pytest-cov + python -m pytest tests --junit-xml pytest.xml macOS: - name: macOS runs-on: [macos-latest] steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install . - - - name: Test with pytest - run: | - pip install pytest - pip install pytest-cov - python -m pytest tests --junit-xml pytest.xml \ No newline at end of file + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + + - name: Test with pytest + run: | + pip install pytest + pip install pytest-cov + python -m pytest tests --junit-xml pytest.xml diff --git a/Makefile b/Makefile index 2390acd..9e05248 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,54 @@ SHELL:=/bin/bash -.PHONY: pypi -pypi: dist - twine upload dist/* +.DEFAULT_GOAL=_help + +# NOTE: must put a character and two pound "\t##" to show up in this list. Keep it brief! IGNORE_ME +.PHONY: _help +_help: + @printf "\nUsage: make , valid commands:\n\n" + @grep "##" $(MAKEFILE_LIST) | grep -v IGNORE_ME | sed -e 's/##//' | column -t -s $$'\t' + -.PHONY: dist -dist: flake8 +.PHONY: build +build: lint ## Build release +build: -rm dist/* ./setup.py sdist bdist_wheel -.PHONY: flake8 -flake8: - flake8 . --exclude '*venv,build' --count --select=E901,E999,F821,F822,F823 --show-source --statistics - flake8 . --exclude '*venv,build' --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - # CI pipeline - flake8 . --exclude='*venv,build' --ignore=E741,E501 +.PHONY: release +release: build ## Upload release to PyPI (via Twine) + twine upload dist/* + + + +LINT_LOCS_PY ?= ffpass/ scripts tests/ + +.PHONY: format +format: ## Not phased in yet, no-op + -black --check ${LINT_LOCS_PY} + -isort --check ${LINT_LOCS_PY} + + +.PHONY: lint +lint: ## Lint the code + flake8 --count --show-source --statistics -.PHONY: install -install: - pip install . .PHONY: test -test: - @echo 'Remember to run make install to test against the latest :)' - coverage run -m pytest -svv tests/ - coverage report -m --omit="tests/*" +test: ## Run pytest & show coverage report + coverage run + coverage report + + + +.PHONY: install +install: ## Install from local source (via pip) + pip install . .PHONY: clean -clean: +clean: ## Clean up build files/cache rm -rf *.egg-info build dist rm -f .coverage find . \ diff --git a/ffpass/__init__.py b/ffpass/__init__.py index e7f7a09..3184934 100755 --- a/ffpass/__init__.py +++ b/ffpass/__init__.py @@ -50,6 +50,8 @@ from pyasn1.codec.der.encoder import encode as der_encode from pyasn1.type.univ import Sequence, OctetString, ObjectIdentifier from Crypto.Cipher import AES, DES3 +# noqa: W503 +# line break before binary operator MAGIC1 = b"\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01" @@ -345,7 +347,7 @@ def getJsonLogins(directory): def dumpJsonLogins(directory, jsonLogins): with open(directory / "logins.json", "w") as loginf: - json.dump(jsonLogins, loginf, separators=",:") + json.dump(jsonLogins, loginf, separators=(",", ":")) def exportLogins(key, jsonLogins): @@ -390,11 +392,13 @@ def readCSV(csv_file): break # Heuristic: if it lacks a URL (index=1) and has user,pass (index=2,3), assume it's a header and continue if ( - "http://" not in first_row[0] - and first_row[1].lower() in {"username", "uname", "user", "u"} # noqa: W503 line break before binary operator - and first_row[2].lower() in {"password", "passwd", "pass", "p"} # noqa: W503 + first_row[0].lower() in {"url", "hostname", "website", "site", "address", "link"} + or ( + first_row[1].lower() in {"username", "uname", "user", "u"} + and first_row[2].lower() in {"password", "passwd", "pass", "p"} + ) ): - logging.debug(f"Continuing (skipping) over first row: [is_header={True}].") + logging.debug(f"Continuing (skipping) over first row: index=0, [is_header={True}].") continue # ~~~ END peek at first row ~~~~~~~~~~ @@ -594,11 +598,13 @@ def makeParser(): import argcomplete argcomplete.autocomplete(parser) - except ModuleNotFoundError: - sys.stderr( - "NOTE: You can run 'pip install argcomplete' " + except (ImportError, ModuleNotFoundError): + sys.stderr.write( + "Error: argcomplete not found, run 'pip install argcomplete' " "and add the hook to your shell RC for tab completion." ) + sys.stderr.write(os.linesep) + sys.stderr.flush() return parser diff --git a/requirements-dev.txt b/requirements-dev.txt index ded13e8..64ce1ef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ coverage==7.13.0 flake8==7.3.0 pytest==9.0.2 - diff --git a/requirements.txt b/requirements.txt index 9a633f2..b7383ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ argcomplete>=3.5.2 pyasn1~=0.6.1 pycryptodome~=3.23.0 - diff --git a/scripts/generate_mp_profile.py b/scripts/generate_mp_profile.py index 59d7e04..2db9257 100755 --- a/scripts/generate_mp_profile.py +++ b/scripts/generate_mp_profile.py @@ -14,7 +14,6 @@ from hashlib import sha1 from pathlib import Path from Crypto.Cipher import AES, DES3 -# Dependencies: pyasn1, pycryptodome from pyasn1.codec.der.encoder import encode as der_encode from pyasn1.type.univ import Integer, ObjectIdentifier, OctetString, Sequence diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..af3409b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,81 @@ +[tool:pytest] +# See: https://docs.pytest.org/en/7.1.x/reference/customize.html +testpaths = + tests + +[coverage:run] +# See: https://coverage.readthedocs.io/en/7.2.2/config.html#run +command_line = -m pytest -svv +source = ffpass + +[coverage:report] +fail_under = 75.00 +precision = 2 + +show_missing = True +skip_empty = True +skip_covered = True + +exclude_lines = + pragma: no cover + + + +[flake8] +exclude = .venv,venv,build,dist + +max-complexity = 10 +max-line-length = 127 + +ignore = + # line break before binary operator + W503, + # line too long + E501, + # ambiguous variable name + E741, + + + +[isort] +line_length = 88 +known_first_party = ntclient + +# See: https://copdips.com/2020/04/making-isort-compatible-with-black.html +multi_line_output = 3 +include_trailing_comma = True + + + +[mypy] +show_error_codes = True +;show_error_context = True +;pretty = True + +disallow_incomplete_defs = True +disallow_untyped_defs = True +disallow_untyped_calls = True +disallow_untyped_decorators = True + +;strict_optional = True +no_implicit_optional = True + +warn_return_any = True +warn_redundant_casts = True +warn_unreachable = True + +warn_unused_ignores = True +warn_unused_configs = True +warn_incomplete_stub = True + +# Our tests, they don't return a value typically +[mypy-tests.*] +disallow_untyped_defs = False + +# Our packages, nested dependencies +; [mypy-] +; ignore_missing_imports = True + +# 3rd party packages missing types +[mypy-argcomplete] +ignore_missing_imports = True diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_mixed_keys_run.py b/tests/test_mixed_keys_run.py index e77ed02..5d9582e 100644 --- a/tests/test_mixed_keys_run.py +++ b/tests/test_mixed_keys_run.py @@ -6,7 +6,6 @@ Created on Fri Dec 25 19:30:48 2025 @author: shane """ -import os import shutil import sys from pathlib import Path @@ -14,7 +13,6 @@ from unittest.mock import patch import pytest -OS_NEWLINE = os.linesep HEADER = "url,username,password" EXPECTED_MIXED_OUTPUT = [HEADER, "http://www.mixedkeys.com,modern_user,modern_pass"] diff --git a/tests/test_run.py b/tests/test_run.py index b6aea28..801a56a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 -import os import subprocess import shutil -import pytest from pathlib import Path -OS_NEWLINE = os.linesep +import pytest MASTER_PASSWORD = 'test' HEADER = 'url,username,password' -IMPORT_CREDENTIAL = 'http://www.example.com,foo,bar' +IMPORT_CREDENTIAL = 'https://www.example.com,foo,bar' EXPECTED_EXPORT_OUTPUT = [HEADER, 'http://www.stealmylogin.com,test,test'] EXPECTED_IMPORT_OUTPUT = EXPECTED_EXPORT_OUTPUT + [IMPORT_CREDENTIAL] @@ -29,11 +27,11 @@ def clean_profile(tmp_path): return _setup -def run_ffpass(mode, path): - command = ["python", "./ffpass/__init__.py", mode, "-d", str(path)] +def run_ffpass_cmd(mode, path): + command = ["python", "./ffpass/__init__.py", mode, "--debug", "--dir", str(path)] if mode == 'import': - ffpass_input = OS_NEWLINE.join([HEADER, IMPORT_CREDENTIAL]) + ffpass_input = "\n".join([HEADER, IMPORT_CREDENTIAL]) else: ffpass_input = None @@ -45,14 +43,14 @@ def stdout_splitter(input_text): def test_legacy_firefox_export(clean_profile): - r = run_ffpass('export', clean_profile('firefox-70')) + r = run_ffpass_cmd('export', clean_profile('firefox-70')) r.check_returncode() actual_export_output = stdout_splitter(r.stdout) assert actual_export_output == EXPECTED_EXPORT_OUTPUT def test_firefox_export(clean_profile): - r = run_ffpass('export', clean_profile('firefox-84')) + r = run_ffpass_cmd('export', clean_profile('firefox-84')) r.check_returncode() assert stdout_splitter(r.stdout) == EXPECTED_EXPORT_OUTPUT @@ -60,7 +58,7 @@ def test_firefox_export(clean_profile): def test_firefox_aes_export(clean_profile): # This uses your new AES-encrypted profile profile_path = clean_profile('firefox-146-aes') - r = run_ffpass('export', profile_path) + r = run_ffpass_cmd('export', profile_path) r.check_returncode() assert stdout_splitter(r.stdout) == EXPECTED_EXPORT_OUTPUT @@ -69,10 +67,10 @@ def test_legacy_firefox(clean_profile): profile_path = clean_profile('firefox-70') # modifies the temp file, not the original - r = run_ffpass('import', profile_path) + r = run_ffpass_cmd('import', profile_path) r.check_returncode() - r = run_ffpass('export', profile_path) + r = run_ffpass_cmd('export', profile_path) r.check_returncode() assert stdout_splitter(r.stdout) == EXPECTED_IMPORT_OUTPUT @@ -80,9 +78,20 @@ def test_legacy_firefox(clean_profile): def test_firefox(clean_profile): profile_path = clean_profile('firefox-84') - r = run_ffpass('import', profile_path) + r = run_ffpass_cmd('import', profile_path) + r.check_returncode() + + r = run_ffpass_cmd('export', profile_path) + r.check_returncode() + assert stdout_splitter(r.stdout) == EXPECTED_IMPORT_OUTPUT + + +def test_firefox_aes(clean_profile): + profile_path = clean_profile('firefox-146-aes') + + r = run_ffpass_cmd('import', profile_path) r.check_returncode() - r = run_ffpass('export', profile_path) + r = run_ffpass_cmd('export', profile_path) r.check_returncode() assert stdout_splitter(r.stdout) == EXPECTED_IMPORT_OUTPUT -- 2.52.0