From dd8955af7b9de99d2f2892348ca1eea7276f8940 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sun, 14 Aug 2022 14:19:59 -0600 Subject: [PATCH] Setup GitHub Actions (#7) * ci: utilizes GitHub Actions Uses GitHub actions for Pylint, CodeQL analysis, and automated PyPI uploads --- .github/FUNDING.yml | 1 + .github/dependabot.yml | 13 ++++++ .github/workflows/codeql.yml | 72 +++++++++++++++++++++++++++++++++ .github/workflows/pylint.yml | 23 +++++++++++ .github/workflows/pypi.yml | 39 ++++++++++++++++++ .github/workflows/test-pypi.yml | 42 +++++++++++++++++++ .gitignore | 2 +- .pylintrc | 4 +- README.md | 4 ++ pfsense_vshell/__init__.py | 38 ++++++++--------- scripts/pfsense-vshell | 1 + setup.py | 50 +++++++++++++++++++---- tests/test_vshell.py | 2 +- 13 files changed, 261 insertions(+), 30 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/pylint.yml create mode 100644 .github/workflows/pypi.yml create mode 100644 .github/workflows/test-pypi.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e0ef325 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: jaredhendrickson13 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6496356 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + reviewers: + - "jaredhendrickson13" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3b76a08 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '37 5 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..bbc0406 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,23 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') \ No newline at end of file diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..6dedd77 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,39 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test-pypi.yml b/.github/workflows/test-pypi.yml new file mode 100644 index 0000000..ce78baa --- /dev/null +++ b/.github/workflows/test-pypi.yml @@ -0,0 +1,42 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Test PyPI + +on: + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: | + export __PFSENSE_VSHELL_DEVREVISION__="${GITHUB_RUN_ID}" + python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ diff --git a/.gitignore b/.gitignore index 336844e..a9028cc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ build* *__pycache__* *pfsense_vshell.egg* dist* -venv* \ No newline at end of file +venv* diff --git a/.pylintrc b/.pylintrc index 3c23876..1ce0a0b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -82,7 +82,7 @@ persistent=yes # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.5 +py-version=3.10 # Discover python modules and packages in the file system subtree. recursive=no @@ -335,7 +335,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=128 +max-line-length=120 # Maximum number of lines in a module. max-module-lines=1000 diff --git a/README.md b/README.md index d765008..339a940 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![PyPI](https://github.com/jaredhendrickson13/pfsense-vshell/actions/workflows/pypi.yml/badge.svg)](https://github.com/jaredhendrickson13/pfsense-vshell/actions/workflows/pypi.yml) +[![PyLint](https://github.com/jaredhendrickson13/pfsense-vshell/actions/workflows/pylint.yml/badge.svg)](https://github.com/jaredhendrickson13/pfsense-vshell/actions/workflows/pylint.yml) +[![CodeQL](https://github.com/jaredhendrickson13/pfsense-vshell/actions/workflows/codeql.yml/badge.svg)](https://github.com/jaredhendrickson13/pfsense-vshell/actions/workflows/codeql.yml) + # Introduction pfSense vShell is a command line tool and Python module that enables users to remotely enter shell commands on a pfSense host without enabling `sshd`. This allows administrators to automate installation of packages, enable `sshd`, and make other backend diff --git a/pfsense_vshell/__init__.py b/pfsense_vshell/__init__.py index dee3ede..14a7c61 100644 --- a/pfsense_vshell/__init__.py +++ b/pfsense_vshell/__init__.py @@ -11,17 +11,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Defines the client object used to establish virtual pfSense shell connections.""" +__version__ = "2.0.4" - -# IMPORT MODULES # import datetime import html + import requests import urllib3 class PFClient: """Client object that facilitates controlling the virtual shell.""" + # Allow current number of instance attributes, they are needed to allow configurable connections + # pylint: disable=too-many-instance-attributes def __init__(self, host, username, password, port=443, scheme="https", timeout=30, verify=True): """ @@ -34,6 +37,8 @@ def __init__(self, host, username, password, port=443, scheme="https", timeout=3 :param timeout: (int) the timeout value in seconds for HTTP requests. Defaults to 30. :param verify: (bool) true to enable certificate verification, false to disable. Defaults to true. """ + # Allow current number of arguments, it does not affect readability + # pylint: disable=too-many-arguments # Set properties using parameters self.session = requests.Session() @@ -57,7 +62,7 @@ def version(): Provides the current version of pfsense vShell :return: (string) the current pfSense vShell version """ - return "2.0.3" + return __version__ def url(self): """ @@ -146,14 +151,16 @@ def authenticate(self): if "username or Password incorrect" not in req.text and "class=\"fa fa-sign-out\"" in req.text: self.__log__("authenticate", "success") return True - elif "

One moment while the initial setup wizard starts." in req.text: + # Support first time logings where wizard is triggered + if "

One moment while the initial setup wizard starts." in req.text: self.__log__("authenticate", "success") return True - else: - self.__log__("authenticate", "failed") - return False - else: - return True + # Otherwise, assume authentication failed + self.__log__("authenticate", "failed") + return False + + # Don't re-authenticate if we're already authenticated + return True def get_csrf_token(self, uri): """ @@ -183,7 +190,7 @@ def has_dns_rebind_error(self, req=None): """ # Make a preliminary request to check if a DNS Rebind error was detected by pfSense. resp = req.text if req else self.request("/").text - return True if "Potential DNS Rebind attack detected" in resp else False + return "Potential DNS Rebind attack detected" in resp def is_host_pfsense(self, req=None): """ @@ -208,7 +215,7 @@ def is_host_pfsense(self, req=None): for item in check_items: platform_confidence = platform_confidence + 10 if item in resp else platform_confidence - return True if platform_confidence > 50 else False + return platform_confidence > 50 def __has_host_errors__(self): """ @@ -259,10 +266,5 @@ def __log__(self, event, msg): self.log.append(",".join([str(datetime.datetime.utcnow()), self.url(), self.username, event, msg])) -class PFError(Exception): - """ - Error object used by the PFVShell class - """ - def __init__(self, code, message): - self.code = code - self.message = message +class PFError(BaseException): + """Error object used by the PFVShell class""" diff --git a/scripts/pfsense-vshell b/scripts/pfsense-vshell index d7e02c6..bfc32a4 100644 --- a/scripts/pfsense-vshell +++ b/scripts/pfsense-vshell @@ -18,6 +18,7 @@ import argparse import sys import time import pfsense_vshell +import pkg_resources class PFCLI: diff --git a/setup.py b/setup.py index d521b3d..4fcf338 100644 --- a/setup.py +++ b/setup.py @@ -11,13 +11,46 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Sets up the pfsense-vshell package for distribution.s""" + +import codecs +import os from setuptools import setup -def read_me(): - with open('README.md') as f: - return f.read() +def read(rel_path): + """Reads a specified file.""" + here = os.path.abspath(os.path.dirname(__file__)) + with codecs.open(os.path.join(here, rel_path), 'r') as filepath: + return filepath.read() + + +def get_version(rel_path): + """ + Gets the current version of the package. If a __PFSENSE_VSHELL_DEVREVISION__ environment variable exists, it will + be read and appended to the current package version. This is used to ensure the setup version can always be unique + for PyPI dev builds triggered by CI/CD workflows. + """ + # Variables + revision = "" + + # If a __PFSENSE_VSHELL_DEVREVISION__ environment variable exists, set it as the dev revision. + if "__PFSENSE_VSHELL_DEVREVISION__" in os.environ: + revision = "." + os.environ.get("__PFSENSE_VSHELL_DEVREVISION__") + + # Otherwise, look for the version in the package. + for line in read(rel_path).splitlines(): + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + revision + + raise RuntimeError("Unable to find version string.") + + +def get_readme(): + """Reads the README.md for this repository to include in package distributions.""" + return read("README.md") setup( @@ -26,15 +59,16 @@ def read_me(): author_email='jaredhendrickson13@gmail.com', url="https://github.com/jaredhendrickson13/pfsense-vshell", license="Apache-2.0", - description="A command line tool to run remote shell commands on pfSense without SSH", - long_description=read_me(), + description="A command line tool to run remote shell commands on pfSense without SSH.", + long_description=get_readme(), long_description_content_type="text/markdown", - version="2.0.3", + version=get_version("pfsense_vshell/__init__.py"), scripts=['scripts/pfsense-vshell'], packages=["pfsense_vshell"], install_requires=[ - "requests", - "urllib3" + "requests~=2.28.1", + "urllib3~=1.26.10", + "pylint~=2.14.5" ], classifiers=[ "Programming Language :: Python :: 3", diff --git a/tests/test_vshell.py b/tests/test_vshell.py index eea29c7..1696d11 100644 --- a/tests/test_vshell.py +++ b/tests/test_vshell.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +"""Tests the pfsense_vshell package.""" import unittest import os