diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..15d21e3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,302 @@ +################################# +## PYTHON STATIC CODE ANALYSIS ## +## Reusable Workflow ## +################################# + +# Static Code Analysis (SCA) is a set of techniques for examining source code +# without executing it. + +### TOOLS ### +# - Ruff +# - Isort +# - Black +# - Pyflakes, Pyroma, McCabe, DodgyRun, Profile Validator +# - Pylint (legacy) + +## AUTONOMOUS JOB ## + +# 0. Never run +# 1. Always run +# 2. Run SQA on conditions +# - Triggered on a Long-living branch (ie main) +# - Triggered on a v* tag (ie v1.0.0) +# - Source Code changed, compared to previous commit +# 3. Run SQA, if Source Code changed, compared to previous commit + +on: + workflow_call: + inputs: + # Defaults to Policy 2 (CI/CD) + run_policy: + required: false + type: string + default: '2' + dedicated_branches: + required: false + type: string + default: 'master, main, dev' + source_code_targets: + required: false + type: string + default: 'src' + ## Parametrizing Runtime Environment (ie py version) + python_version: + required: false + type: string + default: '3.10' + ## Parametrizing Code Analysis Acceptance Criteria + pylint_threshold: + required: false + type: string + default: '8.0' # out of 10 + ## Parametrize Bandit Acceptance Criteria + bandit: + required: false + type: string + description: 'Bandit Acceptance Criteria' + # ie { "l": 10 } -> allowes at most 10 low, 0 m, and 0 h + # ie { "l": 10, "m": 3 } -> allowes at most 10 low, 3 m, and 0 h + # Default is 6 allowed low, 0 m, and 0 h + default: '{"l": 8, "m": 0, "h": 0}' + +jobs: + # Decide whether to run Lint Job, given incoming Signal + lint_policy: + name: "Run Lint Job?" + runs-on: ubuntu-latest + if: always() && inputs.run_policy != 0 + steps: + - if: ${{ !contains('1, 2, 3', inputs.run_policy) }} + run: 'echo "Invalid run_policy: ${{ inputs.run_policy }}. Must be >0 and <4" && exit 1' + + - if: inputs.run_policy == 1 + name: 'POLICY: 1 -> Trigger' + run: echo "SHOULD_RUN_SQA=true" >> $GITHUB_ENV + + - if: inputs.run_policy == 2 && contains(inputs.dedicated_branches, github.ref_name) + name: 'POLICY: 2 & Branch: ${{ github.ref_name }} -> Trigger' + run: echo "SHOULD_RUN_SQA=true" >> $GITHUB_ENV + + - if: inputs.run_policy == 2 && startsWith(github.ref, 'refs/tags/v') + name: 'POLICY: 2 & Tag: ${{ github.ref_name }} -> Trigger' + run: echo "SHOULD_RUN_SQA=true" >> $GITHUB_ENV + + - if: ${{ env.SHOULD_RUN_SQA != 'true' && contains('2, 3', inputs.run_policy) }} + name: 'POLICY: 2, 3 -> Derive from DIFF' + run: echo "SHOULD_DERIVE_FROM_DIFF=true" >> $GITHUB_ENV + + - if: ${{ env.SHOULD_DERIVE_FROM_DIFF }} + name: 'POLICY: 2, 3 -> Checkout Code to compute DIFF' + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - if: ${{ env.SHOULD_DERIVE_FROM_DIFF }} + name: 'POLICY: 2, 3 -> Check Source Code DIFF' + run: | + echo "============ List Modified Files ============" + git diff --name-only HEAD^ HEAD + CHANGED_FILES=$(git diff --name-only HEAD^ HEAD) + + # Read Folders we 'Watch' for changes + TARGETS=$(echo "${{ inputs.source_code_targets }}" | tr ',' '\n') + + # Loop through the Watched Folders + for TARGET in $TARGETS; do + # if rel path of changed file matches glob pattern + if [[ $CHANGED_FILES == *"$TARGET"* ]]; then + echo "SHOULD_RUN_SQA=true" >> $GITHUB_ENV + echo "QA Target: $TARGET found in CHANGED_FILES!" + break + fi + done + + ### OUTPUT of JOB ### + - name: "Set 'Run Static Code Analysis' Signal to ${{ env.SHOULD_RUN_SQA }}" + id: set_sqa_signal + run: echo "RUN_SQA=${{ env.SHOULD_RUN_SQA }}" >> $GITHUB_OUTPUT + outputs: + RUN_SQA: ${{ steps.set_sqa_signal.outputs.RUN_SQA }} + + lint: + name: "Static Code Analysis" + runs-on: ubuntu-latest + needs: lint_policy + if: always() && needs.lint_policy.outputs.RUN_SQA == 'true' + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ inputs.python_version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox==3.28 + + ## Isort ## + - name: 'Isort: Require Semantic and Alphabetic order of the Python Imports' + if: ${{ matrix.platform != 'windows-latest' }} + run: tox -e isort -vv -s false + + ## Black ## + - name: 'Black: Require Project Style "opinion" to be followed by the Python Code' + if: ${{ matrix.platform != 'windows-latest' }} + run: tox -e black -vv -s false + + ## Ruff ## + - name: 'Ruff: Require Project to pass Ruff Checks' + run: tox -e ruff -vv -s false + + ## Pyflakes, Pyroma, McCabe, DodgyRun, Profile Validator ## + - name: Run tox -e prospector + if: ${{ matrix.platform != 'windows-latest' }} + run: tox -e prospector -vv -s false + + bandit: + name: "Bandit: Security Analysis" + runs-on: ubuntu-latest + needs: lint_policy + if: always() && needs.lint_policy.outputs.RUN_SQA == 'true' + env: + _SARIF_FILE: results-sarif.json + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ inputs.python_version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox==3.28 + + ## Run BANDIT to identify CWEs ## + - name: 'Bandit: Security Analysis' + run: tox -e bandit -vv -s false -- -q -f sarif -o "${{ env._SARIF_FILE }}" || 'true' + + - name: Upload artifact + uses: actions/upload-artifact@main + with: + name: ${{ env._SARIF_FILE }} + path: ${{ env._SARIF_FILE }} + if-no-files-found: error + + - uses: github/codeql-action/init@v3 + with: + # config-file: ./.github/codeql/codeql-config.yml # or ${{ vars.VAR_CONTENT }} ! + config: | + paths-ignore: + - 'src/cookiecutter_python/{{ cookiecutter.project_slug }}/' + - 'src/cookiecutter_python/{{ cookiecutter.project_slug }}/**' + - 'src/cookiecutter_python/{{ cookiecutter.project_slug }}/**/*' + - 'src/cookiecutter_python/{{ cookiecutter.project_slug }}/*' + - 'src/cookiecutter_python/{{ cookiecutter.project_slug }}/tests/test_cli.py' + + # disable-default-queries: true + # queries: + # - uses: security-extended + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ env._SARIF_FILE }} + + - name: 'Parse Bandit Results, and prepare comparing LOW, MEDIUM, and HIGH' + run: | + # Parse Totals Object + TOTALS=$(jq '.runs[0].properties.metrics._totals' "${{ env._SARIF_FILE }}") + + # Find CWE's of HIGH Severity + HIGH=$(echo $TOTALS | jq '.["SEVERITY.HIGH"]') + + # Find CWE's of MEDIUM Severity + MEDIUM=$(echo $TOTALS | jq '.["SEVERITY.MEDIUM"]') + + # Find CWE's of LOW Severity + LOW=$(echo $TOTALS | jq '.["SEVERITY.LOW"]') + + # Report and Env + echo HIGH=$HIGH >> $GITHUB_ENV + echo MEDIUM=$MEDIUM >> $GITHUB_ENV + echo LOW=$LOW >> $GITHUB_ENV + + - name: Write to Job Step Summary + run: | + echo "## Bandit - Results" >> $GITHUB_STEP_SUMMARY + echo "HIGH: $HIGH" >> $GITHUB_STEP_SUMMARY + echo "MEDIUM: $MEDIUM" >> $GITHUB_STEP_SUMMARY + echo "LOW: $LOW" >> $GITHUB_STEP_SUMMARY + + - name: Destructure Input Acceptance Thresholds + run: | + MAX_HIGH=$(echo '${{ inputs.bandit }}' | jq -r '.h') + MAX_MEDIUM=$(echo '${{ inputs.bandit }}' | jq -r '.m') + MAX_LOW=$(echo '${{ inputs.bandit }}' | jq -r '.l') + + echo "## User Acceptable - Thresholds" >> $GITHUB_STEP_SUMMARY + echo "HIGH: $MAX_HIGH" >> $GITHUB_STEP_SUMMARY + echo "MEDIUM: $MAX_MEDIUM" >> $GITHUB_STEP_SUMMARY + echo "LOW: $MAX_LOW" >> $GITHUB_STEP_SUMMARY + + echo MAX_HIGH=$MAX_HIGH >> $GITHUB_ENV + echo MAX_MEDIUM=$MAX_MEDIUM >> $GITHUB_ENV + echo MAX_LOW=$MAX_LOW >> $GITHUB_ENV + + #### JOB STATUS #### + - name: "If we found more than ${{ env.MAX_HIGH }} HIGH Severity CWE's, Fail!" + if: ${{ env.MAX_HIGH < env.HIGH }} + run: | + echo "Bandit Failed! | Found more HIGH Severity CWE's in code than allowed/acceptable amount." + echo "[ERROR] **HIGH**: $HIGH > $MAX_HIGH" >> $GITHUB_STEP_SUMMARY + exit 1 + + - name: "If we found more than ${{ env.MAX_MEDIUM }} MEDIUM Severity CWE's, Fail!" + if: ${{ env.MAX_MEDIUM < env.MEDIUM }} + run: | + echo "Bandit Failed! | Found more MEDIUM Severity CWE's in code than allowed/acceptable amount." + echo "[ERROR] **MEDIUM**: $MEDIUM > $MAX_MEDIUM" >> $GITHUB_STEP_SUMMARY + exit 1 + + - name: "If we found more than ${{ env.MAX_LOW }} LOW Severity CWE's, Fail!" + if: ${{ env.MAX_LOW < env.LOW }} + run: | + echo "Bandit Failed! | Found more LOW Severity CWE's in code than allowed/acceptable amount." + echo "[ERROR] **LOW**: $LOW > $MAX_LOW" >> $GITHUB_STEP_SUMMARY + exit 1 + + - run: echo "**Bandit Passed!**" >> $GITHUB_STEP_SUMMARY + + legacy_pylint: + runs-on: ubuntu-latest + needs: lint_policy + if: always() && needs.lint_policy.outputs.RUN_SQA == 'true' + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ inputs.python_version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox==3.28 + + ## Pylint (legacy) ## + - name: Run Pylint tool on Python Code Base + run: tox -e pylint -vv -s false | tee pylint-result.txt + + - run: cat pylint-result.txt + + - name: 'Check Pylint Score > ${{ inputs.pylint_threshold }}/10' + if: ${{ matrix.platform != 'windows-latest' }} + run: | + SCORE=`sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' pylint-result.txt` + echo "SCORE -> $SCORE" + # threshold check + if awk "BEGIN {exit !($SCORE >= ${{ inputs.pylint_threshold }})}"; then + echo "PyLint Passed! | Score: ${SCORE} out of 10 | Threshold: ${{ inputs.pylint_threshold }}" + else + echo "PyLint Failed! | Score: ${SCORE} out of 10 | Threshold: ${{ inputs.pylint_threshold }}" + exit 1 + fi diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8ada35c..8af9166 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -39,7 +39,7 @@ env: PUBLISH_ON_PYPI: "true" DRAW_DEPENDENCIES: "true" PREVENT_CODECOV_TEST_COVERAGE: "false" - DOCKER_JOB_ON: "true" + DOCKER_JOB_ON: "false" ############################### #### DOCKER Job Policy #####