+---
name: ffpass
on: [push, pull_request_target]
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
+---
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
SHELL:=/bin/bash
-.PHONY: pypi
-pypi: dist
- twine upload dist/*
+.DEFAULT_GOAL=_help
+
+# NOTE: must put a <TAB> character and two pound "\t##" to show up in this list. Keep it brief! IGNORE_ME
+.PHONY: _help
+_help:
+ @printf "\nUsage: make <command>, 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 . \
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"
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):
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 ~~~~~~~~~~
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
coverage==7.13.0
flake8==7.3.0
pytest==9.0.2
-
argcomplete>=3.5.2
pyasn1~=0.6.1
pycryptodome~=3.23.0
-
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
--- /dev/null
+[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-<pkgname>]
+; ignore_missing_imports = True
+
+# 3rd party packages missing types
+[mypy-argcomplete]
+ignore_missing_imports = True
@author: shane
"""
-import os
import shutil
import sys
from pathlib import Path
import pytest
-OS_NEWLINE = os.linesep
HEADER = "url,username,password"
EXPECTED_MIXED_OUTPUT = [HEADER, "http://www.mixedkeys.com,modern_user,modern_pass"]
#!/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]
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
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
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
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
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