Skip to main content Home Creators educlopez duck-ai ps-security-audit
ps-security-audit Sets up weekly automated security scanning for PrestaShop 8 projects hosted on GitLab.com. Checks: installed modules vs Friends of Presta advisory database, PrestaShop core version, and PHP/Composer dependencies via Trivy. Sends weekly HTML email report every Monday. Use ONLY for Cinetic PrestaShop projects on GitLab.com. Triggers when user asks for security scanning, vulnerability alerts, module CVE check, or Friends of Presta integration on a PrestaShop project.
Jump to install Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/educlopez/duck-ai --skill ps-security-auditThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository Sets up the full Cinetic security stack on Cinetic projects hosted on GitLab.com (non-PrestaShop: Laravel, Astro, TanStack, etc.). Use ONLY when the project is a Cinetic company project on GitLab.com Free tier. Triggers when the user asks to add dependency scanning, vulnerability alerts, security setup, Trivy, pnpm supply chain protection, or wants email reports of vulnerabilities. Do NOT use for GitHub-hosted projects, personal projects, or PrestaShop projects — use ps-security-audit skill instead for any PrestaShop project.
Sets up BrowserSync live-reload watcher for PrestaShop Panda child theme development in Lando. Auto-detects child theme name and Lando proxy URL. Watches child theme CSS/JS/TPL files and reloads the browser automatically on save. Use when working on PS frontend/maquetación in Lando and the user wants auto-reload, live reload, watcher, or is tired of reloading the browser manually after CSS changes.
Sets up a static image placeholder for local Lando PrestaShop development so broken/missing product images show a nice placeholder instead of broken icons. Use this skill when the user says "placeholder de imagenes en lando", "lando image placeholder", "imagenes rotas en lando", "broken images in lando", or wants to avoid downloading hundreds of product images for local development. Trigger proactively whenever the user is working on a Lando PrestaShop project and mentions broken images or missing product photos.
Related occupations SOC
Based on SOC occupation classification
name ps-security-audit description Sets up weekly automated security scanning for PrestaShop 8 projects hosted on GitLab.com. Checks: installed modules vs Friends of Presta advisory database, PrestaShop core version, and PHP/Composer dependencies via Trivy. Sends weekly HTML email report every Monday. Use ONLY for Cinetic PrestaShop projects on GitLab.com. Triggers when user asks for security scanning, vulnerability alerts, module CVE check, or Friends of Presta integration on a PrestaShop project.
version 0.1.0
PrestaShop Security Audit
Weekly automated security scan for PrestaShop 8 projects on GitLab.com Free tier.
Covers:
Friends of Presta (FoP) module advisory check — cross-references installed modules against known CVEs
PrestaShop core version — detects outdated core vs latest stable
Trivy — scans PHP/Composer dependencies for CVEs and leaked secrets
Weekly HTML email — Monday 8am Madrid, same format as other Cinetic projects
What's unique about PrestaShop security
The biggest attack vector in recent PS vulnerabilities is third-party modules (marketplace and premium themes) with SQL injection, path traversal, and file upload flaws. Friends of Presta maintains the authoritative CVE list at:
GitHub: https://github.com/friends-of-presta/security-advisories
Site: https://security.friendsofpresta.org
Most attacks exploit id_cart, id_product, id_address parameters without pSQL() sanitization in module controllers. Trivy does NOT catch these — only FoP cross-referencing does.
Step 1 — GitLab CI Job
Create or update .gitlab-ci.yml in the project root:
ps-security-audit:
image:
name: aquasec/trivy:latest
entrypoint: ["" ]
before_script:
- apk add --no-cache curl python3 py3-packaging py3-yaml tar
script:
- trivy fs --exit-code 0 --scanners vuln,secret --format json -o trivy-report.json . 2 >/dev/null
- trivy fs --exit-code 0 --scanners vuln,secret --format template --template "@/contrib/html.tpl" -o trivy-report.html . 2 >/dev/null || true
- |
python3 << 'PYEOF'
import json, os, re, glob, tarfile, urllib.request, yaml
from packaging.version import Version, InvalidVersion
{ } { } { } { }
{ }
{}
[]
{ }
{ }
{ , , , , }
[]
[]
[]
[]
[ ]
[ ]
{ }
[ , , , , , }
, []
, []
, , ,
,
,
,
[ ] { [ ], [ ], , [], []}
[ ]
, [ ],
[ ]
[ ]
[ ]
[]
[ ]
,
[ ] , [ ] [ ]
[ ],
{ } , ,
, ,
, ,
, ,
, ,
}
{
, ,
, ,
, ,
, ,
, ,
}
,
,
,
,
,
{ } { }
{ } { }
{ }
{ } ]
{ { [ ]} [ ] }
{ [ ] [ , , , ]}
{ [ ][ ]}
[
{ } { } { } { } { }
,
,
,
]
{ [ ]} { [ ]} { [ ]} { [ ] } { }
[
,
,
,
,
,
]
{ [ ]} { [ ]} { } { [ ]} { }
[
,
,
,
]
{ } { }
{ } { }
{ }
{ }
{ }
{ }
{ }
{ }
{ }
{ } { } { }
Step 2 — GitLab CI/CD Variables
Settings → CI/CD → Variables en el proyecto:
Variable Value Masked GMAIL_USERcuenta@gmail.comNo GMAIL_APP_PASSApp password Google (16 chars) Yes
Cómo obtener App Password Gmail:
Google Account → Security → 2-Step Verification (debe estar ON)
Buscar "App passwords" → Create → nombre "GitLab CI"
Copiar los 16 caracteres → pegar como GMAIL_APP_PASS
Step 3 — Pipeline Schedule (Monday 8am Madrid)
Via GitLab API (una sola vez por proyecto):
curl --request POST \
--header "PRIVATE-TOKEN: <tu-gitlab-token>" \
"https://gitlab.com/api/v4/projects/<PROJECT_ID>/pipeline_schedules" \
--form "description=Weekly PS security scan" \
--form "ref=main" \
--form "cron=0 7 * * 1" \
--form "cron_timezone=Europe/Madrid"
O via UI: CI/CD → Schedules → New schedule
Test manual (tras crear el schedule):
curl --request POST \
--header "PRIVATE-TOKEN: <token>" \
"https://gitlab.com/api/v4/projects/<PROJECT_ID>/pipeline_schedules/<SCHEDULE_ID>/play"
Step 4 — PS project structure assumptions
El job asume estructura estándar PS8:
modules/<name>/<name>.php — módulos instalados (lee $this->version)
config/defines.inc.php — versión del core (_PS_VERSION_)
vendor/ — dependencias Composer (Trivy lo escanea automáticamente)
Si los módulos están en path distinto, ajustar el glob en el script Python.
Interpreting results
FoP module hits:
Si fixed_versions existe → actualizar módulo en PS backoffice o via ZIP desde proveedor
Si fixed_versions está vacío → módulo abandonado, evaluar desactivar y buscar alternativa
Si versión instalada no aparece en affected_versions → FoP lo lista pero revisar manualmente
Trivy PHP hits:
symfony/* → composer update "symfony/*" --with-all-dependencies
Paquete específico → composer update <vendor/package>
composer audit para detalle local
PS Core outdated:
Seguir guía oficial de actualización: backup DB + files → actualizar vía 1-click o CLI
Checklist para nuevo proyecto PS
Notes
entrypoint: [""] en la imagen Trivy es obligatorio — la imagen no tiene shell por defecto
--exit-code 0 en Trivy — pipeline nunca falla, siempre envía email
FoP descarga ~50MB de tar.gz en cada run; normal, no hay API paginada más ligera
PyYAML viene pre-instalado en Alpine Python (py3-yaml), no necesita pip
Módulos sin $this->version en su PHP (raro) reportan unknown — no se cruzan con FoP
ps_version
=
"unknown"
defines_file
=
"config/defines.inc.php"
if os.path.exists(defines_file):
with
open(defines_file,
encoding="utf-8",
errors="ignore")
as f:
m
=
re.search(r"_PS_VERSION_['\"]?\s*,\s*['\"]([^'\"]+)['\"]",
f.read())
if m:
ps_version
=
m.group(1)
ps_latest
=
"unknown"
try:
req
=
urllib.request.Request(
"https://api.github.com/repos/PrestaShop/PrestaShop/releases/latest"
,
headers={"User-Agent":
"ps-security-audit-ci"
}
)
with
urllib.request.urlopen(req,
timeout=10)
as r:
release
=
json.loads(r.read().decode())
ps_latest
=
release.get("tag_name",
"unknown"
).lstrip("v")
except Exception as e:
print(f"Warning:
could not fetch PS latest version:
e
")
ps_outdated = False
try:
ps_outdated = Version(ps_version) < Version(ps_latest)
except InvalidVersion:
pass
print(f"
PS core:
ps_version
|
Latest:
ps_latest
|
Outdated:
ps_outdated
")
# ── 2. Installed modules + versions ──────────────────────────────
installed = {}
for mod_dir in glob.glob("
modules/*/"):
mod_name
=
os.path.basename(mod_dir.rstrip("/"))
php_file
=
os.path.join(mod_dir,
f"{mod_name}.php")
if os.path.exists(php_file):
with
open(php_file,
encoding="utf-8",
errors="ignore")
as f:
content
=
f.read()
m
=
re.search(r"\$this->version\s*=\s*['\"]([^'\"]+)['\"]",
content)
installed[mod_name]
=
m.group(1)
if
m
else
"unknown"
print(f"Found
len(installed)
modules
installed")
advisories
=
try:
urllib.request.urlretrieve(
"https://github.com/friends-of-presta/security-advisories/archive/refs/heads/main.tar.gz"
,
"fop.tar.gz"
)
with
tarfile.open("fop.tar.gz")
as tar:
for member in tar.getmembers():
if
"/_modules/"
in
member.name
and
member.name.endswith(".md"):
f
=
tar.extractfile(member)
if not f:
continue
content
=
f.read().decode("utf-8",
errors="ignore")
if
not
content.startswith("---"):
continue
parts
=
content.split("---",
2
)
if
len(parts)
<
3:
continue
try:
meta
=
yaml.safe_load(parts[1])
if
not
isinstance(meta,
dict):
continue
mod_name
=
meta.get("module",
""
)
if mod_name:
advisories.setdefault(mod_name,
).append(meta)
except Exception:
pass
print(f"Loaded
len(advisories)
FoP
advisories")
except Exception as e:
print(f"Warning:
could not fetch FoP advisories:
e
")
# ── 4. Cross-reference modules vs advisories ──────────────────────
def is_affected(installed_ver, affected_versions):
try:
iv = Version(str(installed_ver))
for spec in (affected_versions or []):
spec = str(spec).strip()
if spec.startswith("
<=
"):
if iv <= Version(spec[3:].strip()): return True
elif spec.startswith("
<
"):
if iv < Version(spec[2:].strip()): return True
elif spec.startswith("
>=
"):
if iv >= Version(spec[3:].strip()): return True
elif spec.startswith("
==
") or spec.startswith("
=
"):
if iv == Version(spec.lstrip("
=").strip()):
return
True
except InvalidVersion:
pass
return
False
severity_order
=
"critical":
0
"high":
1
"medium":
2
"low":
3
"unknown":
4
vulnerable_modules
=
for
mod_name,
installed_ver in installed.items():
if mod_name not in advisories:
continue
for
advisory
in
advisories[mod_name]:
affected
=
advisory.get("affected_versions",
)
if
not
affected
or
is_affected(installed_ver,
affected):
fixed
=
advisory.get("fixed_versions",
)
fixed_str
=
", "
.join(str(v)
for
v
in
fixed)
if
fixed
else
"No fix yet"
cves
=
advisory.get("additional_cve")
or
if
isinstance(cves,
str):
cves
=
cves
sev
=
str(advisory.get("severity",
"unknown"
)).lower()
vulnerable_modules.append({
"module":
mod_name,
"installed":
installed_ver,
"fixed":
fixed_str,
"severity":
sev,
"cvss":
advisory.get("cvss_base_score",
""
),
"cves":
str(c)
for
c
in
cves
,
"title":
advisory.get("title",
""
),
})
vulnerable_modules.sort(key=lambda
x:
severity_order.get(x["severity"],
4
))
print(f"Vulnerable
modules found:
len(vulnerable_modules)
")
# ── 5. Parse Trivy results ────────────────────────────────────────
trivy_vulns = []
try:
with open("
trivy-report.json")
as f:
trivy_data
=
json.load(f)
from
packaging.version
import
Version
as
V,
InvalidVersion
as
IE
def best_fix(fixed_str):
if
not
fixed_str
or
fixed_str
==
"N/A":
return
None
parts
=
p.strip()
for
p
in
fixed_str.split("
") if p.strip()]
parsed = []
for p in parts:
try: parsed.append((V(p), p))
except IE: parsed.append((V("
0
"), p))
return max(parsed, key=lambda x: x[0])[1] if parsed else None
grouped = {}
trivy_sev_order = {"
CRITICAL":
0
"HIGH":
1
"MEDIUM":
2
"LOW":
3
"UNKNOWN":
4
for
result
in
trivy_data.get("Results"
):
for
v
in
result.get("Vulnerabilities"
):
key
=
(v.get("PkgName"
""
)
v.get("InstalledVersion"
""
))
sev
=
v.get("Severity"
"UNKNOWN"
)
cve
=
v.get("VulnerabilityID"
""
)
fix
=
best_fix(v.get("FixedVersion"
""
))
if key not in grouped:
grouped
key
=
"pkg":
key
0
"installed":
key
1
"severity":
sev
"cves":
"fixes":
e
=
grouped
key
if
trivy_sev_order.get(sev
5
)
<
trivy_sev_order.get(e
"severity"
5):
e
"severity"
=
sev
if cve:
e
"cves"
.append(cve)
if fix:
e
"fixes"
.append(fix)
for entry in grouped.values():
parsed
=
for
v
in
entry
"fixes"
:
try:
parsed.append((V(v)
v))
except IE:
pass
entry
"best_fix"
=
max(parsed
key=lambda
x:
x
0
)
1
if
parsed
else
None
trivy_vulns.append(entry)
trivy_vulns.sort(key=lambda
x:
trivy_sev_order.get(x
"severity"
5
))
except Exception as e:
print(f"Warning:
could not parse Trivy results:
e
")
# ── 6. Build email ────────────────────────────────────────────────
fop_colors = {
"
critical":
("#ffeef0"
"#d73a49"
)
"high":
("#fff3cd"
"#856404"
)
"medium":
("#e8f4fd"
"#0c5460"
)
"low":
("#f0f0f0"
"#555"
)
"unknown":
("#f0f0f0"
"#555"
)
trivy_colors
=
"CRITICAL":
("#ffeef0"
"#d73a49"
)
"HIGH":
("#fff3cd"
"#856404"
)
"MEDIUM":
("#e8f4fd"
"#0c5460"
)
"LOW":
("#f0f0f0"
"#555"
)
"UNKNOWN":
("#f0f0f0"
"#555"
)
project
=
os.environ.get("CI_PROJECT_NAME"
""
)
branch
=
os.environ.get("CI_COMMIT_REF_NAME"
""
)
sha
=
os.environ.get("CI_COMMIT_SHORT_SHA"
""
)
pipeline_url
=
os.environ.get("CI_PIPELINE_URL"
"#"
)
gmail_user
=
os.environ.get("GMAIL_USER"
""
)
ps_badge_color
=
"#ffeef0"
if
ps_outdated
else
"#e6ffed"
ps_badge_text_color
=
"#d73a49"
if
ps_outdated
else
"#22863a"
ps_status
=
f"⚠️
OUTDATED
—
latest
is
ps_latest
" if ps_outdated else f"
✓
Up
to
date
(
ps_latest
)"
ps_section
=
f"""
<div
style='background:
ps_badge_color
;border:1px
solid
ps_badge_text_color
;border-radius:6px;padding:12px
16px;margin-bottom:20px'>
<strong>PrestaShop
Core:</strong>
ps_version
|
<strong
style='color:
ps_badge_text_color
'>{ps_status}</strong>
</div>"""
# FoP module table
fop_rows = ""
for v in vulnerable_modules:
bg, fg = fop_colors.get(v["severity"], ("#fff", "#333"))
cves_str = ", ".join(v["cves"][:3]) + (f" +{len(v['
cves'
)-3}
more"
if
len(v["cves"])
>
3
else
""
)
fop_rows
+=
f"""<tr>
<td
style='padding:8px;border-bottom:1px
solid
<td
style='padding:8px;border-bottom:1px
solid
<td
style='padding:8px;border-bottom:1px
solid
<td
style='padding:8px;border-bottom:1px
solid
<span
style='background:{bg};color:{fg};padding:2px
8px;border-radius:4px;font-weight:bold;font-size:12px'>{v['severity'].upper()}</span>
f"<small
style='color:#888;margin-left:4px'>CVSS
v
'cvss'
</small>"
if
v
'cvss'
else
""
</td>
<td
style='padding:8px;border-bottom:1px
solid
</tr>"""
fop_table
=
""
if
not
vulnerable_modules
else
f"""
<h3
style='color:#24292e;margin-top:24px'>Vulnerable
Modules
(Friends
of
Presta)</h3>
<table
style='width:100%;border-collapse:collapse;margin-top:10px'>
<thead>
<tr
style='background:#f6f8fa'>
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
</tr>
</thead>
<tbody>{fop_rows}</tbody>
</table>"""
fop_ok
=
""
if
vulnerable_modules
else
"<p style='color:green;font-weight:bold'>✓ No vulnerable modules found.</p>"
trivy_counts
=
s:
sum(1
for
v
in
trivy_vulns
if
v
"severity"
==
s)
for
s
in
"CRITICAL"
"HIGH"
"MEDIUM"
"LOW"
trivy_rows
=
""
for v in trivy_vulns:
bg,
fg
=
trivy_colors.get(v["severity"],
("#fff",
"#333"
))
fixed
=
v["best_fix"]
if
v["best_fix"]
else
"<em style='color:#888'>No fix yet</em>"
cves_str
=
", "
.join(v["cves"][:3])
+
(f"
+{len(v['cves'])-3}
more"
if
len(v["cves"])
>
3
else
""
)
trivy_rows
+=
f"""<tr>
<td
style='padding:8px;border-bottom:1px
solid
<td
style='padding:8px;border-bottom:1px
solid
<td
style='padding:8px;border-bottom:1px
solid
<td
style='padding:8px;border-bottom:1px
solid
<span
style='background:{bg};color:{fg};padding:2px
8px;border-radius:4px;font-weight:bold;font-size:12px'>{v['severity']}</span>
</td>
<td
style='padding:8px;border-bottom:1px
solid
</tr>"""
trivy_badges
=
""
.join([
f"<span
style='display:inline-block;padding:6px
12px;border-radius:6px;font-weight:bold;margin-right:8px;background:{trivy_colors[s][0]};color:{trivy_colors[s][1]};border:1px
solid
trivy_colors
s
1
'>{s}: {trivy_counts[s]}</span>"
for s in ["CRITICAL", "HIGH", "MEDIUM", "LOW"] if trivy_counts.get(s, 0) > 0
])
trivy_section = "" if not trivy_vulns else f"""
<h3 style='
color:#24292e;margin-top:24px'>PHP/Composer
Dependencies
(Trivy)</h3>
<p>{trivy_badges
or
"<span style='color:green;font-weight:bold'>✓ No dependency vulnerabilities found.</span>"
}</p>
<table
style='width:100%;border-collapse:collapse;margin-top:10px'>
<thead>
<tr
style='background:#f6f8fa'>
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
<th
style='padding:10px;text-align:left;border-bottom:2px
solid
</tr>
</thead>
<tbody>{trivy_rows}</tbody>
</table>"""
md_lines
=
f"#
PS
Security
Scan
—
project
",
f"
Branch:
branch
|
Commit:
sha
",
f"
PrestaShop core:
ps_version
|
Latest:
ps_latest
",
"
",
"
""
"| Module | Installed | Fix | Severity | CVEs |"
"|--------|-----------|-----|----------|------|"
for v in vulnerable_modules:
cves
=
", "
.join(v["cves"])
md_lines.append(f"|
v
'module'
|
v
'installed'
|
v
'fixed'
|
v
'severity'
.upper()
|
cves
|")
md_lines
+=
""
"## PHP/Composer Dependencies (Trivy)"
""
"| Package | Installed | Fix | Severity | CVEs |"
"|---------|-----------|-----|----------|------|"
for v in trivy_vulns:
fix
=
v["best_fix"]
if
v["best_fix"]
else
"No fix yet"
cves
=
", "
.join(v["cves"])
md_lines.append(f"|
v
'pkg'
|
v
'installed'
|
fix
|
v
'severity'
|
cves
|")
md_lines
+=
""
"## Task"
"Review PS security findings. For vulnerable modules: check if update is available in marketplace or if module should be disabled. For Composer deps: provide exact update command."
md_content
=
"\n"
.join(md_lines)
with
open("ps-security-report.md",
"w"
)
as f:
f.write(md_content)
md_escaped
=
md_content.replace("&",
"&"
).replace("<",
"<"
).replace(">",
">"
)
md_section
=
f"""
<hr
style='margin:30px
0
;border:none;border-top:1px
solid
<h3
style='color:#24292e'>Paste
to
Claude</h3>
<p
style='font-size:13px;color:#555'>Copia
el
bloque
y
pégalo
en
Claude
para
análisis
y
fixes:</p>
<pre
style='background:#f6f8fa;padding:15px;border-radius:6px;font-size:11px;white-space:pre-wrap;border:1px
solid
fop_count
=
len(vulnerable_modules)
trivy_critical
=
trivy_counts.get("CRITICAL",
0
)
trivy_high
=
trivy_counts.get("HIGH",
0
)
subject
=
f"[{project}]
PS
Security
—
fop_count
módulos
vulnerables,
trivy_critical
CVE
críticos"
html
=
f"""From:
gmail_user
\r\nTo:
eduardo@cineticdigital.com,
dgalera@cineticdigital.com\r\nSubject:
subject
\r\nMIME-Version:
1.0
\r\nContent-Type:
text/html;
charset=UTF-8\r\n\r\n
<!DOCTYPE
html><html><head></head><body
style='font-family:Arial,sans-serif;max-width:900px;margin:0
auto;padding:20px;color:#333'>
<h2
style='color:#24292e'>PrestaShop
Security
Scan
—
project
</h2>
<p>Branch:
<strong>{branch}</strong>
|
Commit:
<code>{sha}</code></p>
ps_section
<p><strong>FoP
modules vulnerables:
fop_count
</strong></p>
fop_ok
fop_table
trivy_section
md_section
<p
style='margin-top:20px'>
<a
href='{pipeline_url}'
style='display:inline-block;padding:10px
20px;background:#1f75cb;color:white;text-decoration:none;border-radius:4px'>
Ver
Pipeline
y
Descargar
Artefactos
</a>
</p>
<p
style='font-size:12px;color:#888;margin-top:20px'>Generado
por
Trivy
+
Friends
of
Presta</p>
</body></html>"""
with
open("email_body.txt",
"w"
)
as f:
f.write(html)
print(f"Report
ready:
fop_count
vulnerable
modules,
trivy_critical
CRITICAL
/
trivy_high
HIGH
PHP
deps")
PYEOF
-
|
curl --url "smtps://smtp.gmail.com:465" \
--ssl-reqd \
--mail-from "$GMAIL_USER" \
--mail-rcpt "eduardo@cineticdigital.com" \
--mail-rcpt "dgalera@cineticdigital.com" \
--user "$GMAIL_USER:$GMAIL_APP_PASS" \
-T email_body.txt
artifacts:
paths:
-
trivy-report.html
-
trivy-report.json
-
ps-security-report.md
expire_in:
30
days
rules:
-
if:
$CI_PIPELINE_SOURCE
==
"schedule"