fix(ci): rendre la chaine d'integration reellement verifiante
Les quatre jobs existaient ; aucun ne verifiait ce qu'il annoncait.
security-audit
`pip-audit --require-hashes --no-deps || pip-audit`. L'etape
d'installation ne posait que pip-audit, et aucune des deux formes ne
nommait le fichier d'exigences : le repli auditait l'environnement du
runner, qui ne contenait que pip-audit lui-meme. Le job passait au vert
sans avoir examine une seule dependance de l'application. Remplace par
`pip-audit -r requirements.txt`.
security-scan
Appelait `python security_scan.py`, alors que le fichier se trouve dans
app/supporting_scripts/. En echec a chaque execution depuis le
deplacement du fichier. Trois autres defauts sont apparus en le faisant
tourner :
- la CI passe --skip-http, un argument que l'argparse du script
n'acceptait pas : sortie en erreur 2 meme avec le bon chemin.
- check_dependencies lisait data['dependencies'] comme la liste des
vulnerabilites. Ce tableau liste en realite TOUTES les dependances,
chacune portant un champ vulns vide si le paquet est sain. Les ~45
paquets installes etaient donc signales vulnerables a chaque
execution. Le filtrage se fait desormais sur vulns non vide.
- check_flask_config interceptait son exception et renvoyait quand
meme all_ok : ne pas reussir a charger l'application comptait comme
un controle reussi. La section la plus importante du rapport n'avait
jamais tourne. Elle renvoie desormais False, et l'import fonctionne
grace a l'ajout de la racine du projet dans sys.path.
- la banniere en caracteres semi-graphiques faisait planter le script
sur une console Windows en cp1252, la plateforme meme du projet.
Passee en ASCII.
lint
Ruff n'avait aucun fichier de configuration : le job tournait sur le jeu
de regles par defaut. La configuration vit maintenant dans pyproject.toml.
`ruff format --check` est retire pour l'instant : la base n'ayant jamais
ete formatee, il echouerait sur 62 fichiers sur 64 pour des raisons
etrangeres a la correction. Reformatage puis application : QUA-002.
test
Un `echo` protege par continue-on-error : le job annoncait un succes
sans rien executer. Il lance desormais pytest avec couverture, et bloque.
permissions: contents: read au niveau du workflow, aucune etape n'ecrivant
dans le depot.
Deploiement Gitea
actions/checkout@v7 n'existe pas (derniere majeure : v5) : le workflow
echouait des sa premiere etape. Ramene a v4.
Le miroir lftp poussait l'integralite de l'arbre de travail, dont
clear_db.py -- un script qui vide toutes les tables et recree
admin/password -- vers le noeud de production. Liste d'exclusions ajoutee.
--delete reste volontairement absent : les contrats televerses, les
journaux et le .env du serveur vivent sous la racine de deploiement et
sont absents du depot ; les supprimer detruirait des donnees.
Le workflow de deploiement n'a pas pu etre execute depuis ici : la
syntaxe lftp reste a valider lors du prochain deploiement manuel.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -11,8 +11,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
# Was @v7, which does not exist (latest major is v5): the workflow
|
||||||
|
# failed on its very first step.
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install lftp and ssh
|
- name: Install lftp and ssh
|
||||||
run: sudo apt-get update && sudo apt-get install -y lftp openssh-client
|
run: sudo apt-get update && sudo apt-get install -y lftp openssh-client
|
||||||
@@ -29,14 +31,36 @@ jobs:
|
|||||||
|
|
||||||
- name: Push files via SFTP with progress
|
- name: Push files via SFTP with progress
|
||||||
run: |
|
run: |
|
||||||
# The mirror command below uses the -R (reverse) flag
|
# The mirror command below uses the -R (reverse) flag
|
||||||
# to push from local './' to remote './'
|
# to push from local './' to remote './'
|
||||||
# Connection is made using 'open' inside the execution block to enforce SSH key usage
|
# Connection is made using 'open' inside the execution block to enforce SSH key usage
|
||||||
|
#
|
||||||
|
# --exclude-glob entries: the previous command mirrored the entire
|
||||||
|
# working tree, so CI definitions, the test suite and clear_db.py --
|
||||||
|
# a script that DELETEs every table and recreates admin/password --
|
||||||
|
# were all shipped to the production node.
|
||||||
|
#
|
||||||
|
# --delete is deliberately NOT used. Uploaded contracts, logs and the
|
||||||
|
# server's own .env live under the deployment root and are absent
|
||||||
|
# from the repository; deleting anything not present locally would
|
||||||
|
# destroy them. Stale files therefore accumulate: switching to an
|
||||||
|
# atomic timestamped-directory deploy is tracked as OPS-011.
|
||||||
lftp -e "set sftp:connect-program 'ssh -a -x -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -o BatchMode=yes -o PasswordAuthentication=no'; \
|
lftp -e "set sftp:connect-program 'ssh -a -x -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -o BatchMode=yes -o PasswordAuthentication=no'; \
|
||||||
set sftp:auto-confirm yes; \
|
set sftp:auto-confirm yes; \
|
||||||
set net:max-retries 5; \
|
set net:max-retries 5; \
|
||||||
set net:timeout 30; \
|
set net:timeout 30; \
|
||||||
set cmd:fail-exit yes; \
|
set cmd:fail-exit yes; \
|
||||||
open -u ${{ secrets.SSH_USER }}, sftp://sftp.node4.immortal.host:2022; \
|
open -u ${{ secrets.SSH_USER }}, sftp://sftp.node4.immortal.host:2022; \
|
||||||
mirror -R --verbose --parallel=4 ./ ./; \
|
mirror -R --verbose --parallel=4 \
|
||||||
|
--exclude-glob .git/ \
|
||||||
|
--exclude-glob .github/ \
|
||||||
|
--exclude-glob .gitea/ \
|
||||||
|
--exclude-glob .venv/ \
|
||||||
|
--exclude-glob venv/ \
|
||||||
|
--exclude-glob tests/ \
|
||||||
|
--exclude-glob audit/ \
|
||||||
|
--exclude-glob .ai/ \
|
||||||
|
--exclude-glob __pycache__/ \
|
||||||
|
--exclude-glob clear_db.py \
|
||||||
|
./ ./; \
|
||||||
quit"
|
quit"
|
||||||
+38
-18
@@ -11,6 +11,13 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
# Least privilege: nothing here writes back to the repository.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
PYTHON_VERSION: '3.12'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security-audit:
|
security-audit:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
@@ -21,14 +28,19 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install pip-audit
|
||||||
run: pip install pip-audit
|
run: pip install pip-audit==2.9.0
|
||||||
|
|
||||||
- name: Scan for vulnerable dependencies
|
# Previously: `pip-audit --require-hashes --no-deps || pip-audit`.
|
||||||
run: pip-audit --require-hashes --no-deps || pip-audit
|
# Neither form named the requirements file, so the fallback audited the
|
||||||
|
# runner's environment — which contained pip-audit and nothing else.
|
||||||
|
# The job passed green while checking none of the application's
|
||||||
|
# dependencies. -r makes it audit what the application actually pins.
|
||||||
|
- name: Scan declared dependencies for known vulnerabilities
|
||||||
|
run: pip-audit -r requirements.txt
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint with Ruff
|
name: Lint with Ruff
|
||||||
@@ -39,16 +51,20 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Install ruff
|
- name: Install ruff
|
||||||
run: pip install ruff
|
run: pip install ruff==0.14.4
|
||||||
|
|
||||||
|
# Rule selection and per-file ignores live in pyproject.toml. Before it
|
||||||
|
# existed, this step ran ruff's bare defaults with no configuration.
|
||||||
- name: Run ruff linter
|
- name: Run ruff linter
|
||||||
run: ruff check . --output-format=github
|
run: ruff check . --output-format=github
|
||||||
|
|
||||||
- name: Run ruff formatter check
|
# `ruff format --check` is deliberately absent for now: the codebase has
|
||||||
run: ruff format --check .
|
# never been formatted, so it would fail on 62 of 64 files for reasons
|
||||||
|
# unrelated to correctness. Reformatting in one isolated commit and then
|
||||||
|
# enforcing it here is tracked as QUA-002.
|
||||||
|
|
||||||
security-scan:
|
security-scan:
|
||||||
name: Security Scan
|
name: Security Scan
|
||||||
@@ -59,17 +75,20 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: Install app dependencies
|
- name: Install app dependencies
|
||||||
run: pip install -r requirements.txt
|
run: pip install -r requirements.txt -r requirements-dev.txt
|
||||||
|
|
||||||
|
# The path was `security_scan.py`, but the script lives under
|
||||||
|
# app/supporting_scripts/. The step had therefore failed on every run
|
||||||
|
# since the file was moved.
|
||||||
- name: Run security scan
|
- name: Run security scan
|
||||||
env:
|
env:
|
||||||
SECRET_KEY: ${{ secrets.CI_SECRET_KEY || 'test-key-not-for-production-1234567890' }}
|
SECRET_KEY: ${{ secrets.CI_SECRET_KEY || 'test-key-not-for-production-1234567890' }}
|
||||||
FLASK_DEBUG: 'false'
|
FLASK_DEBUG: 'false'
|
||||||
run: python security_scan.py --skip-http
|
run: python app/supporting_scripts/security_scan.py --skip-http
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Tests
|
name: Tests
|
||||||
@@ -81,14 +100,15 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install -r requirements.txt
|
run: pip install -r requirements.txt -r requirements-dev.txt
|
||||||
|
|
||||||
|
# Previously an `echo` guarded by continue-on-error: the job reported
|
||||||
|
# success without executing anything. The suite needs no environment
|
||||||
|
# variables and no database server: create_app() takes its configuration
|
||||||
|
# as an argument and the fixtures use a temporary SQLite file.
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: pytest --cov=app --cov-report=term-missing --cov-report=xml
|
||||||
echo "No tests configured yet. Add tests to the project."
|
|
||||||
# python -m pytest tests/ --cov=. --cov-report=xml
|
|
||||||
continue-on-error: true
|
|
||||||
|
|||||||
@@ -177,10 +177,21 @@ def check_dependencies():
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
data = json.loads(result.stdout)
|
data = json.loads(result.stdout)
|
||||||
vulns = data.get('dependencies', [])
|
# pip-audit's "dependencies" array lists EVERY dependency, each
|
||||||
if vulns:
|
# carrying a "vulns" list that is empty when the package is
|
||||||
for vuln in vulns:
|
# clean. Treating the array itself as the vulnerability list
|
||||||
print(f'[FAIL] {vuln["name"]}=={vuln["version"]}: {vuln.get("description", "Vulnerability found")}')
|
# reported all ~45 installed packages as vulnerable on every
|
||||||
|
# run, which is why this check was pure noise.
|
||||||
|
affected = [
|
||||||
|
dep for dep in data.get('dependencies', [])
|
||||||
|
if dep.get('vulns')
|
||||||
|
]
|
||||||
|
if affected:
|
||||||
|
for dep in affected:
|
||||||
|
ids = ', '.join(
|
||||||
|
v.get('id', '?') for v in dep.get('vulns', [])
|
||||||
|
)
|
||||||
|
print(f'[FAIL] {dep["name"]}=={dep["version"]}: {ids}')
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
print('[OK] No vulnerabilities found')
|
print('[OK] No vulnerabilities found')
|
||||||
@@ -264,9 +275,20 @@ def check_flask_config():
|
|||||||
all_ok = True
|
all_ok = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# The script lives two levels below the project root; without this the
|
||||||
|
# import fails and the whole check was silently skipped.
|
||||||
|
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
if root not in sys.path:
|
||||||
|
sys.path.insert(0, root)
|
||||||
|
|
||||||
from app.app import create_app
|
from app.app import create_app
|
||||||
app = create_app()
|
# Inspect configuration only: no schema creation, no Discord bot.
|
||||||
|
app = create_app({
|
||||||
|
'SQLALCHEMY_DATABASE_URI': os.getenv('DATABASE_URL') or 'sqlite:///:memory:',
|
||||||
|
'AUTO_CREATE_TABLES': False,
|
||||||
|
'ENABLE_DISCORD_BOT': False,
|
||||||
|
})
|
||||||
|
|
||||||
# Check session cookie settings
|
# Check session cookie settings
|
||||||
cookie_checks = [
|
cookie_checks = [
|
||||||
('SESSION_COOKIE_SECURE', True, 'Secure cookies'),
|
('SESSION_COOKIE_SECURE', True, 'Secure cookies'),
|
||||||
@@ -313,8 +335,12 @@ def check_flask_config():
|
|||||||
print('[OK] DEBUG mode: disabled')
|
print('[OK] DEBUG mode: disabled')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'[SKIP] Cannot check Flask config: {e}')
|
# Returning all_ok (still True) here meant that failing to load the
|
||||||
|
# application at all was counted as a passing check — the most
|
||||||
|
# important section of the report silently never ran.
|
||||||
|
print(f'[FAIL] Cannot check Flask config: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
return all_ok
|
return all_ok
|
||||||
|
|
||||||
|
|
||||||
@@ -329,22 +355,29 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description='Security validation scanner')
|
parser = argparse.ArgumentParser(description='Security validation scanner')
|
||||||
parser.add_argument('--url', default='http://localhost:5000',
|
parser.add_argument('--url', default='http://localhost:5000',
|
||||||
help='Application URL to check headers (default: http://localhost:5000)')
|
help='Application URL to check headers (default: http://localhost:5000)')
|
||||||
|
parser.add_argument('--skip-http', action='store_true',
|
||||||
|
help='Skip the live HTTP header check (no server running, e.g. in CI)')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════╗')
|
# Plain ASCII: the box-drawing characters this banner used crashed the
|
||||||
print('║ TEAM TRYOUTS - SECURITY VALIDATION SCANNER ║')
|
# script outright on a cp1252 Windows console, which is the platform the
|
||||||
print('╠══════════════════════════════════════════════════════════╣')
|
# project is developed and deployed on.
|
||||||
print(f'║ Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
|
print('=' * 60)
|
||||||
print('╚══════════════════════════════════════════════════════════╝')
|
print('TEAM TRYOUTS - SECURITY VALIDATION SCANNER')
|
||||||
|
print(f'Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
|
||||||
checks = [
|
print('=' * 60)
|
||||||
check_environment,
|
|
||||||
lambda: check_https_headers(args.url),
|
checks = [check_environment]
|
||||||
|
if args.skip_http:
|
||||||
|
print('\n[SKIP] HTTP header check disabled via --skip-http')
|
||||||
|
else:
|
||||||
|
checks.append(lambda: check_https_headers(args.url))
|
||||||
|
checks += [
|
||||||
check_dependencies,
|
check_dependencies,
|
||||||
check_file_permissions,
|
check_file_permissions,
|
||||||
check_flask_config,
|
check_flask_config,
|
||||||
]
|
]
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for check in checks:
|
for check in checks:
|
||||||
results.append(check())
|
results.append(check())
|
||||||
|
|||||||
Reference in New Issue
Block a user