cornelsen-offline-lernen-bin
The package downloads a prebuilt Electron app from the official vendor domain and patches its JavaScript files locally to improve offline functionality; the use of npx is for asar manipulation tools on extracted app code, not remote code execution, and the non-standard host is part of the vendor's own infrastructure.
Triggered rules
llm_review
The static rules flagged this MEDIUM, but an AI model (qwen/qwen3-235b-a22b-2507) reviewed the full PKGBUILD and judged it LOW (confidence 95%): The package downloads a prebuilt Electron app from the official vendor domain and patches its JavaScript files locally to improve offline functionality; the use of npx is for asar manipulation tools on extracted app code, not remote code execution, and the non-standard host is part of the vendor's own infrastructure.
2 higher static findings superseded - not the current verdict (shown for transparency)
remote_code_tool
`npx`/`bunx`/`pnpm dlx`/`deno run <url>` downloads AND runs a remote package at build time — the moral equivalent of piping a download into a shell. Severity downgraded: Node.js consumer context.
-
PKGBUILD:66
npx --yes @electron/asar extract "$_asar_path" "$_tmp_dir" -
PKGBUILD:204
npx --yes @electron/asar pack "$_tmp_dir" "$_asar_path"
source_untrusted_domain
One or more source=() URLs point to a host outside the trusted allowlist (github.com, gitlab.com, codeberg.org, pypi.org, …).
-
PKGBUILD:12
source=("${pkgname}-${pkgver}.zip::https://ebook.cornelsen.de/uma20/public/v2/uma/offline/win"
PKGBUILD
3 offending line(s) highlighted# Maintainer: MicLeh <micleh at proton dot me>
pkgname=cornelsen-offline-lernen-bin
pkgver=37.10.2
# App version 2026.8.1 from 2026-05-07
pkgrel=10
pkgdesc="Cornelsen Offline Lernen Electron App"
arch=('x86_64')
url="https://www.cornelsen.de"
license=('custom')
depends=('electron37')
makedepends=('npm')
source=("${pkgname}-${pkgver}.zip::https://ebook.cornelsen.de/uma20/public/v2/uma/offline/win"
"${pkgname}.sh"
"${pkgname}.desktop"
"icon16.png"
"icon32.png"
"icon96.png")
b2sums=('80a1762e20039c727ee433c559cc0a64f21cf2a33b7e2039e6cc3944b0bcd61724a5f89ae7d7935d41ad40a2d31018f0ea9c039442767e638b8bc669ecf49193'
'9be125c58666538ccd056253a32daa1d55ce38ae72a97e1f5deebaef6c729e8b0a548f8478fa0b1f5b11d7759bc98b4cdd21db07d3b93dfea7cf4e2d760fc8e6'
'8157b061d35da630252a8c402a98fb978fcad59fcfa813eb8b67dfeaae7e467051d659201e55b5d1811ae15eca49e47189a5a9840277f09876cf1d7021cfc1da'
'8dd68d9cd90347b954da9280d2133bc45706eec1e74261bdf19f53b61150ee5168db20a0f217643230c4dc66fb840f9ef61fce5f6aff5853b1bebae4bc9f4c65'
'b326c7f468c3cecb0e2618bae56092b7c6c5fa7e8f43f499645df01c2b468e74052bef51d48b36a8a7ade2fa12f9612591eaf38819998868271ec23abd366ad5'
'302bef8514b991cb6903d2def32abf348329a1cf104009b3c50c0e83e44b15103b89f669fb2012894df0c7d6ab926151f09e0cf61aff4102296be7071f66f794')
_find_asar_path() {
local _path
local _candidates=(
"src/windows/resources/app.asar"
"src/src/windows/resources/app.asar"
"windows/resources/app.asar"
"resources/app.asar"
)
for _path in "${_candidates[@]}"; do
if [ -f "$_path" ]; then
printf '%s\n' "$_path"
return 0
fi
done
_path="$(find . -type f -path '*/resources/app.asar' | head -n 1)"
if [ -n "$_path" ]; then
printf '%s\n' "${_path#./}"
return 0
fi
return 1
}
prepare() {
local _asar_path
_asar_path="$(_find_asar_path || true)"
if [ -z "$_asar_path" ] || [ ! -f "$_asar_path" ]; then
echo "ERROR: source app.asar not found in extracted zip"
exit 1
fi
local _tmp_base _tmp_dir
_tmp_base="${TMPDIR:-/var/tmp}"
if [ ! -w "$_tmp_base" ]; then
_tmp_base="$PWD"
fi
_tmp_dir="$(mktemp -d "$_tmp_base/${pkgname}.asar.XXXXXX")"
npx --yes @electron/asar extract "$_asar_path" "$_tmp_dir"
node - "$_tmp_dir" <<'NODE'
const fs = require('fs');
const path = require('path');
const root = process.argv[2];
function collectJsFiles(dir, out = []) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) {
collectJsFiles(p, out);
} else if (ent.isFile() && ent.name.endsWith('.js')) {
out.push(p);
}
}
return out;
}
const distDir = path.join(root, 'dist');
if (!fs.existsSync(distDir)) {
console.error('ERROR: unpacked asar does not contain a dist directory');
process.exit(1);
}
const jsFiles = collectJsFiles(distDir);
const verbose = process.env.CORNELSEN_PATCH_VERBOSE === '1';
const strict = process.env.CORNELSEN_PATCH_STRICT === '1';
const patches = [
{
name: 'annotations-404-fallback',
regex: /getProductAnnotations\(r,s=!1\)\{return this\.http\.get\(`\$\{s\?os\.getSyncApiBaseUrl\(\):os\.getApiBaseUrl\(\)\}\/pspdf\/annotations\/\$\{r\}`\)\.pipe\(/g,
replaceWith: 'getProductAnnotations(r,s=!1){return this.http.get(`${s?os.getSyncApiBaseUrl():os.getApiBaseUrl()}/pspdf/annotations/${r}`).pipe(ip(y=>s&&y.status===404?hs({}):r0(()=>y)),',
from: 'getProductAnnotations(r,s=!1){return this.http.get(`${s?os.getSyncApiBaseUrl():os.getApiBaseUrl()}/pspdf/annotations/${r}`).pipe(ip(y=>r0(()=>y)),Jl(y=>this.mapOfflinePdfIds(y)))}',
fromAlt: 'getProductAnnotations(r,s=!1){return this.http.get(`${s?ls.getSyncApiBaseUrl():ls.getApiBaseUrl()}/pspdf/annotations/${r}`).pipe(nc(y=>this.mapOfflinePdfIds(y)))}',
toAlt: 'getProductAnnotations(r,s=!1){return this.http.get(`${s?ls.getSyncApiBaseUrl():ls.getApiBaseUrl()}/pspdf/annotations/${r}`).pipe(op(y=>s&&y.status===404?fs({}):r0(()=>y)),nc(y=>this.mapOfflinePdfIds(y)))}',
fromAlt2: 'getProductAnnotations(r,s=!1){return this.http.get(`${s?cs.getSyncApiBaseUrl():cs.getApiBaseUrl()}/pspdf/annotations/${r}`).pipe(nc(y=>this.mapOfflinePdfIds(y)))}',
toAlt2: 'getProductAnnotations(r,s=!1){return this.http.get(`${s?cs.getSyncApiBaseUrl():cs.getApiBaseUrl()}/pspdf/annotations/${r}`).pipe(op(y=>s&&y.status===404?Es({}):r0(()=>y)),nc(y=>this.mapOfflinePdfIds(y)))}',
fromAlt3: 'getProductAnnotations(i,s=!1){return this.http.get(`${s?cs.getSyncApiBaseUrl():cs.getApiBaseUrl()}/pspdf/annotations/${i}`).pipe(nc(y=>this.mapOfflinePdfIds(y)))}',
toAlt3: 'getProductAnnotations(i,s=!1){return this.http.get(`${s?cs.getSyncApiBaseUrl():cs.getApiBaseUrl()}/pspdf/annotations/${i}`).pipe(dp(y=>s&&y.status===404?ws({}):Q1(()=>y)),nc(y=>this.mapOfflinePdfIds(y)))}',
fromAlt4: 'getProductAnnotations(t,r=!1){return this.http.get(`${r?ps.getSyncApiBaseUrl():ps.getApiBaseUrl()}/pspdf/annotations/${t}`).pipe(zl(d=>this.mapOfflinePdfIds(d)))}',
toAlt4: 'getProductAnnotations(t,r=!1){return this.http.get(`${r?ps.getSyncApiBaseUrl():ps.getApiBaseUrl()}/pspdf/annotations/${t}`).pipe(dp(y=>r&&y.status===404?ws({}):Q1(()=>y)),zl(d=>this.mapOfflinePdfIds(d)))}',
to: 'getProductAnnotations(r,s=!1){return this.http.get(`${s?os.getSyncApiBaseUrl():os.getApiBaseUrl()}/pspdf/annotations/${r}`).pipe(dp(y=>s&&y.status===404?ws({}):Q1(()=>y)),Jl(y=>this.mapOfflinePdfIds(y)))}',
alreadyMarker: 's&&y.status===404?ws({}):Q1(()=>y)',
alreadyMarker2: 's&&y.status===404?ws({}):Q1(()=>y)',
alreadyMarker3: 's&&y.status===404?ws({}):Q1(()=>y)',
alreadyMarker4: 'r&&y.status===404?ws({}):Q1(()=>y)',
required: false
},
{
name: 'compatibility-401-fallback',
from: 'isCompatibleWithOnline$(){return this.http.get(`${os.getSyncApiBaseUrl()}/compatibility/offlineClients/${GE}`).pipe(sa(r=>r.isCompatible))}',
fromAlt: 'isCompatibleWithOnline$(){return this.http.get(`${ls.getSyncApiBaseUrl()}/compatibility/offlineClients/${qE}`).pipe(sa(r=>r.isCompatible))}',
toAlt: 'isCompatibleWithOnline$(){return this.http.get(`${ls.getSyncApiBaseUrl()}/compatibility/offlineClients/${qE}`).pipe(sa(r=>r.isCompatible),op(r=>r.status===401?fs(!0):r0(()=>r)))}',
fromAlt2: 'isCompatibleWithOnline$(){return this.http.get(`${cs.getSyncApiBaseUrl()}/compatibility/offlineClients/${ZE}`).pipe(sa(r=>r.isCompatible))}',
toAlt2: 'isCompatibleWithOnline$(){return this.http.get(`${cs.getSyncApiBaseUrl()}/compatibility/offlineClients/${ZE}`).pipe(sa(r=>r.isCompatible),op(r=>r.status===401?Es(!0):r0(()=>r)))}',
fromAlt3: 'isCompatibleWithOnline$(){return this.http.get(`${cs.getSyncApiBaseUrl()}/compatibility/offlineClients/${QE}`).pipe(ta(i=>i.isCompatible))}',
toAlt3: 'isCompatibleWithOnline$(){return this.http.get(`${cs.getSyncApiBaseUrl()}/compatibility/offlineClients/${QE}`).pipe(ta(i=>i.isCompatible),dp(r=>r.status===401?ws(!0):Q1(()=>r)))}',
fromAlt4: 'isCompatibleWithOnline$(){return this.http.get(`${ps.getSyncApiBaseUrl()}/compatibility/offlineClients/${UE}`).pipe(Ea(t=>t.isCompatible))}',
toAlt4: 'isCompatibleWithOnline$(){return this.http.get(`${ps.getSyncApiBaseUrl()}/compatibility/offlineClients/${UE}`).pipe(Ea(t=>t.isCompatible),dp(s=>s.status===401?ws(!0):Q1(()=>s)))}',
to: 'isCompatibleWithOnline$(){return this.http.get(`${os.getSyncApiBaseUrl()}/compatibility/offlineClients/${GE}`).pipe(sa(r=>r.isCompatible),dp(r=>r.status===401?ws(!0):Q1(()=>r)))}',
alreadyMarker: 'r.status===401?ws(!0):Q1(()=>r)',
alreadyMarker2: 'r.status===401?ws(!0):Q1(()=>r)',
alreadyMarker3: 'r.status===401?ws(!0):Q1(()=>r)',
alreadyMarker4: 's.status===401?ws(!0):Q1(()=>s)',
required: false
}
];
for (const p of patches) {
let patched = false;
for (const filePath of jsFiles) {
let txt = fs.readFileSync(filePath, 'utf8');
if (txt.includes(p.to) || (p.toAlt && txt.includes(p.toAlt)) || (p.toAlt2 && txt.includes(p.toAlt2)) || (p.toAlt3 && txt.includes(p.toAlt3)) || (p.toAlt4 && txt.includes(p.toAlt4)) || txt.includes(p.alreadyMarker) || (p.alreadyMarker2 && txt.includes(p.alreadyMarker2)) || (p.alreadyMarker3 && txt.includes(p.alreadyMarker3)) || (p.alreadyMarker4 && txt.includes(p.alreadyMarker4))) {
patched = true;
break;
}
if (p.regex && p.regex.test(txt)) {
txt = txt.replace(p.regex, p.replaceWith);
fs.writeFileSync(filePath, txt);
patched = true;
break;
}
if (txt.includes(p.from)) {
txt = txt.replace(p.from, p.to);
fs.writeFileSync(filePath, txt);
patched = true;
break;
}
if (p.fromAlt && p.toAlt && txt.includes(p.fromAlt)) {
txt = txt.replace(p.fromAlt, p.toAlt);
fs.writeFileSync(filePath, txt);
patched = true;
break;
}
if (p.fromAlt2 && p.toAlt2 && txt.includes(p.fromAlt2)) {
txt = txt.replace(p.fromAlt2, p.toAlt2);
fs.writeFileSync(filePath, txt);
patched = true;
break;
}
if (p.fromAlt3 && p.toAlt3 && txt.includes(p.fromAlt3)) {
txt = txt.replace(p.fromAlt3, p.toAlt3);
fs.writeFileSync(filePath, txt);
patched = true;
break;
}
if (p.fromAlt4 && p.toAlt4 && txt.includes(p.fromAlt4)) {
txt = txt.replace(p.fromAlt4, p.toAlt4);
fs.writeFileSync(filePath, txt);
patched = true;
break;
}
}
if (!patched) {
if (p.required || strict) {
console.error(`ERROR: required patch target not found: ${p.name}`);
process.exit(1);
}
if (verbose) {
console.warn(`INFO: optional patch target not found: ${p.name} (continuing)`);
}
}
}
NODE
npx --yes @electron/asar pack "$_tmp_dir" "$_asar_path"
rm -rf "$_tmp_dir"
}
package() {
local _asar_path
_asar_path="$(_find_asar_path || true)"
if [ ! -f "$_asar_path" ]; then
echo "ERROR: app.asar not found"
exit 1
fi
install -Dm644 "$_asar_path" "$pkgdir/usr/share/$pkgname/app.asar"
install -Dm755 "${pkgname}.sh" "$pkgdir/usr/bin/$pkgname"
install -Dm644 "${pkgname}.desktop" "$pkgdir/usr/share/applications/${pkgname}.desktop"
# Icons
install -Dm644 "icon16.png" "$pkgdir/usr/share/icons/hicolor/16x16/apps/${pkgname}.png"
install -Dm644 "icon32.png" "$pkgdir/usr/share/icons/hicolor/32x32/apps/${pkgname}.png"
install -Dm644 "icon96.png" "$pkgdir/usr/share/icons/hicolor/96x96/apps/${pkgname}.png"
# Licenses
_license=$(find . -name "LICENSE" | head -n 1)
if [ -n "$_license" ]; then
install -Dm644 "$_license" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
fi
}
Scan history
| Scanned at (UTC) | Severity | Rules |
|---|---|---|
| 2026-09-17 00:27:14 | Low | 3 |
| 2026-09-16 00:03:17 | Low | 3 |
| 2026-09-15 00:25:31 | Low | 3 |
| 2026-09-14 00:27:57 | Low | 3 |
| 2026-09-13 00:19:54 | Low | 3 |
| 2026-09-12 00:25:17 | Low | 3 |
| 2026-09-11 00:19:22 | Low | 3 |
| 2026-09-10 00:22:44 | Low | 3 |
| 2026-09-09 00:04:09 | Low | 3 |
| 2026-09-08 00:18:08 | Low | 3 |
| 2026-09-07 00:30:15 | Low | 3 |
| 2026-09-06 00:17:06 | Low | 3 |
| 2026-09-05 00:16:27 | Low | 3 |
| 2026-09-04 00:03:13 | Low | 3 |
| 2026-09-03 00:15:47 | Low | 3 |
| 2026-09-02 00:02:31 | Low | 3 |
| 2026-09-01 00:11:19 | Low | 3 |
| 2026-08-31 00:19:57 | Low | 3 |
| 2026-08-30 00:04:14 | Low | 3 |
| 2026-08-29 00:29:17 | Low | 3 |