Skip to content

Restyle Pinakes and complete mobile collections and catalogue fixes #643

Restyle Pinakes and complete mobile collections and catalogue fixes

Restyle Pinakes and complete mobile collections and catalogue fixes #643

name: Browser Compatibility and DAST
on:
pull_request:
branches: [main]
push:
branches: [main]
schedule:
- cron: '29 2 * * 4'
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
browser-security:
name: Packaged app · 3 browsers · axe-core · OWASP ZAP
runs-on: ubuntu-latest
timeout-minutes: 75
permissions:
contents: read # Build and scan the checked-out application.
services:
mysql:
image: mysql:8.0@sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: pinakes_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1"
--health-interval=10s
--health-timeout=5s
--health-retries=12
env:
E2E_BASE_URL: http://localhost:8081
E2E_ADMIN_EMAIL: admin@pinakes.test
E2E_ADMIN_PASS: Test1234!
E2E_DB_HOST: 127.0.0.1
E2E_DB_PORT: '3306'
E2E_DB_USER: root
E2E_DB_PASS: root
E2E_DB_NAME: pinakes_test
steps:
- name: Checkout without persisted credentials
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Install Apache and packaging tools
run: |
# The hosted runner image ships Google's chrome-stable apt source,
# which these jobs never install from. Remove it before the first
# apt refresh so a mid-sync Google mirror cannot break the job.
sudo rm -f /etc/apt/sources.list.d/google-chrome.list
sudo apt-get update -q
# php-apcu: the app's page cache must run on its production backend
# (APCu shared memory) under Apache/mod_php, not the file fallback.
sudo apt-get install -y apache2 libapache2-mod-php php-apcu jq rsync unzip zip
sudo a2enmod rewrite headers env
php_module=$(find /etc/apache2/mods-available -maxdepth 1 -name 'php*.load' -printf '%f\n' | sed 's/\.load$//' | sort -V | tail -n1)
if [ -n "$php_module" ]; then sudo a2enmod "$php_module"; fi
sudo a2dissite 000-default || true
# Enable APCu for every distro-PHP SAPI (Apache mod_php + distro CLI).
while IFS= read -r conf_dir; do
sudo tee "${conf_dir}/99-pinakes-apcu.ini" >/dev/null <<'EOF'
apc.enable=1
apc.enable_cli=1
EOF
done < <(find /etc/php -type d -path '*/conf.d' | sort -u)
- name: Setup PHP 8.2 tooling
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.2'
extensions: mysqli, pdo_mysql, mbstring, curl, intl, xml, zip, gd, apcu
ini-values: apc.enable=1, apc.enable_cli=1
coverage: none
- name: Setup Node 22
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22'
cache: npm
cache-dependency-path: |
package-lock.json
frontend/package-lock.json
- name: Build the exact release artifact under test
run: |
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
npm ci --silent
npm --prefix frontend ci --silent
npm --prefix frontend run build
bash bin/build-release.sh --skip-build
version=$(jq -r .version version.json)
mkdir -p "$RUNNER_TEMP/pinakes-package"
unzip -q "releases/pinakes-v${version}.zip" -d "$RUNNER_TEMP/pinakes-package"
package_root="$RUNNER_TEMP/pinakes-package/pinakes-v${version}"
test -f "$package_root/public/index.php"
echo "PACKAGE_ROOT=$package_root" >> "$GITHUB_ENV"
echo "E2E_INSTALL_ROOT=$package_root" >> "$GITHUB_ENV"
- name: Install all supported Playwright browser engines
run: npx playwright install chromium firefox webkit --with-deps
- name: Configure Apache for the packaged application
run: |
path="$PACKAGE_ROOT"
while [ "$path" != / ]; do sudo chmod o+x "$path"; path=$(dirname "$path"); done
sudo chmod 777 "$PACKAGE_ROOT"
sudo chmod -R 777 "$PACKAGE_ROOT/storage"
sudo mkdir -p "$PACKAGE_ROOT/public/uploads"
sudo chmod -R 777 "$PACKAGE_ROOT/public/uploads"
sudo tee /etc/apache2/sites-available/pinakes.conf >/dev/null <<EOF
Listen 8081
<VirtualHost *:8081>
ServerName localhost
DocumentRoot ${PACKAGE_ROOT}/public
SetEnv PINAKES_E2E_BYPASS_RATE_LIMIT 1
SetEnv PINAKES_E2E_SCRAPER_STUB 1
# E2E only — arms GET /_e2e/flush-cache so specs that write to the
# DB directly can invalidate the page cache; 404s when unset.
SetEnv PINAKES_E2E_CACHE_FLUSH 1
<Directory "${PACKAGE_ROOT}/public">
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/pinakes-error.log
CustomLog \${APACHE_LOG_DIR}/pinakes-access.log combined
</VirtualHost>
EOF
sudo a2ensite pinakes
sudo apachectl configtest
sudo systemctl start apache2
for attempt in $(seq 1 30); do
curl -sf -o /dev/null http://localhost:8081/installer/ && exit 0
echo "waiting for packaged installer ($attempt/30)"
sleep 1
done
exit 1
- name: Install and bootstrap the packaged application
env:
PLAYWRIGHT_JSON_OUTPUT_FILE: test-results/package-bootstrap.json
PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report-bootstrap
run: npx playwright test tests/full-test.spec.js --config=tests/playwright.ci.config.js --workers=1
- name: Audit installer failures, flakes and skips
if: always()
run: node scripts/ci-audit-playwright-results.js test-results/package-bootstrap.json
- name: Test WCAG and runtime behavior in Chromium, Firefox and WebKit
env:
PLAYWRIGHT_JSON_OUTPUT_FILE: test-results/cross-browser.json
PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report-cross-browser
run: npx playwright test --config=tests/playwright.cross-browser.config.js --workers=1
- name: Audit cross-browser failures, flakes and skips
if: always()
run: node scripts/ci-audit-playwright-results.js test-results/cross-browser.json
- name: Run OWASP ZAP passive baseline scan
uses: zaproxy/action-baseline@de8ad967d3548d44ef623df22cf95c3b0baf8b25 # v0.15.0
with:
target: http://localhost:8081
docker_name: ghcr.io/zaproxy/zaproxy:stable@sha256:781a2bdaea47324e7bab583e2263f21d257b0aee61ed51521a5be45f5f5081ef
cmd_options: -a -m 3
fail_action: false
allow_issue_writing: false
artifact_name: zap-baseline-${{ github.run_id }}
- name: Test narrow ZAP bibliographic PII allowlist
run: bash tests/zap-pii-filter.test.sh
- name: Fail on medium or high ZAP alerts
run: |
set -euo pipefail
identifiers_file="$RUNNER_TEMP/zap-catalogue-identifiers.json"
public_urls_file="$RUNNER_TEMP/zap-public-catalogue-urls.json"
public_examples_file="$RUNNER_TEMP/zap-public-example-identifiers.json"
MYSQL_PWD="$E2E_DB_PASS" mysql \
-h "$E2E_DB_HOST" -P "$E2E_DB_PORT" -u "$E2E_DB_USER" \
-N -B "$E2E_DB_NAME" -e '
SELECT identifier
FROM (
SELECT isbn13 AS identifier FROM libri WHERE deleted_at IS NULL
UNION
SELECT ean AS identifier FROM libri WHERE deleted_at IS NULL
) AS catalogue_identifiers
WHERE identifier REGEXP "^[0-9]{13}$"
ORDER BY identifier
' | jq -Rsc 'split("\n") | map(select(length > 0)) | unique' > "$identifiers_file"
# The single-quoted PHP program is intentional; its $variables must
# reach PHP literally rather than expand in the runner shell.
# shellcheck disable=SC2016
curl -fsS http://localhost:8081/sitemap.xml \
| php -r '
$xml = simplexml_load_string(stream_get_contents(STDIN), SimpleXMLElement::class, LIBXML_NONET);
if ($xml === false) { fwrite(STDERR, "Invalid sitemap XML\n"); exit(2); }
$urls = [];
foreach ($xml->url as $entry) { $urls[] = (string) $entry->loc; }
echo json_encode(array_values(array_unique($urls)), JSON_UNESCAPED_SLASHES), PHP_EOL;
' > "$public_urls_file"
# Translation dictionaries are intentionally inlined client-side.
# Extract only canonical, explicitly named ISBN example keys. The
# checked-in jq program requires a full 978/979 match and a valid
# ISBN-13 checksum, so unrelated numbers and longer substrings cannot
# become origin-wide exceptions.
jq -f scripts/ci-zap-public-isbn-examples.jq \
locale/it_IT.json > "$public_examples_file"
# OWASP ZAP rule 10062 (PII Disclosure) is allowlisted NARROWLY, never
# globally. A book catalogue renders ISBN/EAN-13 identifiers on its
# bibliographic pages, and a 13-digit code inherently collides with the
# credit-card Luhn pattern the rule matches (observed: EAN 4131672754818
# on /de/verlag/Puffin+Books, mis-classified as "Visa" because BIN
# 413167 is a Visa range). An alert is ignored ONLY when EVERY instance
# is a 13-digit number on a bibliographic route — a real card leak
# (15/16 digits) or a 13-digit value on any other page still fails the
# build. The checked-in filter also requires GET with empty param/attack,
# an exact registered route (canonical URLs come from the live sitemap)
# when evidence is a live catalogue identifier. The only route-wide
# exception is the exact public example strings shipped in the inlined
# translation dictionaries. Real secret/PII exposure is also covered
# by secret scanning, Semgrep and CodeQL; every other medium/high alert
# stays blocking.
bash scripts/ci-check-zap-report.sh \
report_json.json "$identifiers_file" "$public_urls_file" "$public_examples_file"
- name: Upload browser and server diagnostics
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: browser-security-evidence-${{ github.run_id }}
path: |
playwright-report-bootstrap/
playwright-report-cross-browser/
test-results/
report_json.json
report_md.md
report_html.html
/var/log/apache2/pinakes-error.log
/var/log/apache2/pinakes-access.log
if-no-files-found: warn
retention-days: 21