feature/meldingen-hardening #6
9 changed files with 755 additions and 385 deletions
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
|
@ -34,3 +34,8 @@ jobs:
|
||||||
# Fail the build if any dependency is not open source.
|
# Fail the build if any dependency is not open source.
|
||||||
- name: Licence compliance (make licenses)
|
- name: Licence compliance (make licenses)
|
||||||
run: make licenses
|
run: make licenses
|
||||||
|
|
||||||
|
# Fail the build if a vendored JS bundle drifted from its manifest or a
|
||||||
|
# pinned version has a known vulnerability (queries the OSV database).
|
||||||
|
- name: Bundled JS security (make deps-check)
|
||||||
|
run: make deps-check
|
||||||
|
|
|
||||||
19
Makefile
19
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: setup format format-check analyze test test-contracts test-preview test-export test-state test-services test-presenter deps-outdated licenses check check-full help
|
.PHONY: setup format format-check analyze test test-contracts test-preview test-export test-state test-services test-presenter deps-outdated deps-check licenses check check-full help
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "OciDeck quality targets:"
|
@echo "OciDeck quality targets:"
|
||||||
|
|
@ -11,6 +11,7 @@ help:
|
||||||
@echo " make test-services Caption/description/image service tests."
|
@echo " make test-services Caption/description/image service tests."
|
||||||
@echo " make test-presenter Fullscreen presenter interaction tests."
|
@echo " make test-presenter Fullscreen presenter interaction tests."
|
||||||
@echo " make deps-outdated Advisory dependency freshness report."
|
@echo " make deps-outdated Advisory dependency freshness report."
|
||||||
|
@echo " make deps-check Verify vendored JS bundles vs manifest + OSV CVEs."
|
||||||
@echo " make licenses Verify all dependencies use open-source licences."
|
@echo " make licenses Verify all dependencies use open-source licences."
|
||||||
|
|
||||||
# Install Flutter/Dart dependencies.
|
# Install Flutter/Dart dependencies.
|
||||||
|
|
@ -106,6 +107,18 @@ deps-outdated:
|
||||||
@echo "Failure means: inspect network/tooling first; outdated packages are not necessarily regressions."
|
@echo "Failure means: inspect network/tooling first; outdated packages are not necessarily regressions."
|
||||||
flutter pub outdated
|
flutter pub outdated
|
||||||
|
|
||||||
|
# Security gate for the vendored JS bundles inlined into the HTML export.
|
||||||
|
# Verifies each file still matches assets/web_export/MANIFEST.json (sha256) and
|
||||||
|
# queries the OSV database for known vulnerabilities in the pinned versions.
|
||||||
|
deps-check:
|
||||||
|
@echo "== OciDeck check: bundled JavaScript =="
|
||||||
|
@echo "Command: dart run tool/check_bundled_js.dart"
|
||||||
|
@echo "Covers: integrity (sha256 vs manifest) + known CVEs (OSV) for marked,"
|
||||||
|
@echo " highlight.js, DOMPurify, mermaid and MathJax."
|
||||||
|
@echo "Failure means: a bundle drifted from the manifest, or a pinned version"
|
||||||
|
@echo " now has a known vulnerability — upgrade it and refresh the manifest."
|
||||||
|
dart run tool/check_bundled_js.dart
|
||||||
|
|
||||||
# Open-source licence compliance check for all resolved dependencies.
|
# Open-source licence compliance check for all resolved dependencies.
|
||||||
licenses:
|
licenses:
|
||||||
@echo "== OciDeck check: licences =="
|
@echo "== OciDeck check: licences =="
|
||||||
|
|
@ -120,6 +133,6 @@ check: format-check analyze test
|
||||||
@echo "Validated: formatting, static analysis, and the full Flutter test suite."
|
@echo "Validated: formatting, static analysis, and the full Flutter test suite."
|
||||||
|
|
||||||
# Extended local check with advisory dependency freshness after the required gate.
|
# Extended local check with advisory dependency freshness after the required gate.
|
||||||
check-full: check licenses deps-outdated
|
check-full: check licenses deps-check deps-outdated
|
||||||
@echo "== OciDeck extended check complete =="
|
@echo "== OciDeck extended check complete =="
|
||||||
@echo "Validated: required quality gate, licence compliance, and dependency freshness."
|
@echo "Validated: required quality gate, licence compliance, bundled-JS CVEs, and dependency freshness."
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,16 @@ Shipped inside the app and embedded into the **offline HTML export**
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [marked](https://github.com/markedjs/marked) | Markdown → HTML in the export | MIT |
|
| [marked](https://github.com/markedjs/marked) | Markdown → HTML in the export | MIT |
|
||||||
| [highlight.js](https://github.com/highlightjs/highlight.js) | Code highlighting in the export | BSD-3-Clause |
|
| [highlight.js](https://github.com/highlightjs/highlight.js) | Code highlighting in the export | BSD-3-Clause |
|
||||||
| [Mermaid](https://github.com/mermaid-js/mermaid) | Diagrams in the export | MIT (bundles [DOMPurify](https://github.com/cure53/DOMPurify), Apache-2.0 / MPL-2.0) |
|
| [DOMPurify](https://github.com/cure53/DOMPurify) | Sanitises the rendered Markdown before it hits the DOM in the export | Apache-2.0 / MPL-2.0 |
|
||||||
|
| [Mermaid](https://github.com/mermaid-js/mermaid) | Diagrams in the export | MIT |
|
||||||
| [MathJax](https://github.com/mathjax/MathJax) (`tex-svg.js`) | Math rendering in the export | Apache-2.0 |
|
| [MathJax](https://github.com/mathjax/MathJax) (`tex-svg.js`) | Math rendering in the export | Apache-2.0 |
|
||||||
| [EB Garamond](https://github.com/octaviopardo/EBGaramond12) font | Bundled deck font | SIL Open Font License 1.1 |
|
| [EB Garamond](https://github.com/octaviopardo/EBGaramond12) font | Bundled deck font | SIL Open Font License 1.1 |
|
||||||
|
|
||||||
|
The exact pinned version, source URL and SHA-256 of every vendored JS bundle
|
||||||
|
live in [`assets/web_export/MANIFEST.json`](assets/web_export/MANIFEST.json).
|
||||||
|
`make deps-check` verifies each file still matches that manifest and queries the
|
||||||
|
[OSV](https://osv.dev) database for known vulnerabilities.
|
||||||
|
|
||||||
## Vendored (forked) plugins
|
## Vendored (forked) plugins
|
||||||
|
|
||||||
Kept in `third_party/` and wired in via `pubspec.yaml` (path dependency /
|
Kept in `third_party/` and wired in via `pubspec.yaml` (path dependency /
|
||||||
|
|
|
||||||
46
assets/web_export/MANIFEST.json
Normal file
46
assets/web_export/MANIFEST.json
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"_comment": "Pinned inventory of the vendored JavaScript bundles inlined into the offline HTML export (see lib/services/marp_html_service.dart). Each entry records the npm package + exact version so `make deps-check` can query the OSV vulnerability database, and a sha256 so the same check can prove the on-disk file still matches this manifest (tamper / accidental-replacement guard). When you intentionally upgrade a bundle, update its version, source and sha256 here in the same commit.",
|
||||||
|
"ecosystem": "npm",
|
||||||
|
"bundles": [
|
||||||
|
{
|
||||||
|
"file": "marked.min.js",
|
||||||
|
"npm": "marked",
|
||||||
|
"version": "18.0.5",
|
||||||
|
"sha256": "2dc4769dfde29f51c7aca1a539c6407c789c8ea644cf8b7d01ded28a9c1d800b",
|
||||||
|
"source": "https://cdn.jsdelivr.net/npm/marked@18.0.5/lib/marked.umd.js",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "highlight.min.js",
|
||||||
|
"npm": "highlight.js",
|
||||||
|
"version": "11.11.1",
|
||||||
|
"sha256": "c4a399dd6f488bc97a3546e3476747b3e714c99c57b9473154c6fb8d259b9381",
|
||||||
|
"source": "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "purify.min.js",
|
||||||
|
"npm": "dompurify",
|
||||||
|
"version": "3.4.9",
|
||||||
|
"sha256": "3c16cc90eb152b823b71b8585cd79e7fb7cd7a380157a800dfbd9459aad5f726",
|
||||||
|
"source": "https://cdn.jsdelivr.net/npm/dompurify@3.4.9/dist/purify.min.js",
|
||||||
|
"license": "Apache-2.0 OR MPL-2.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "mermaid.min.js",
|
||||||
|
"npm": "mermaid",
|
||||||
|
"version": "10.9.6",
|
||||||
|
"sha256": "eda3a0ad572bbe69a318c1be0163e8233dd824f3f12939e5168feba207767151",
|
||||||
|
"source": "https://cdn.jsdelivr.net/npm/mermaid@10.9.6/dist/mermaid.min.js",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "tex-svg.js",
|
||||||
|
"npm": "mathjax",
|
||||||
|
"version": "3.2.2",
|
||||||
|
"sha256": "d4295dc33744836935c1399feece5159577b34c5c8ffb9f1c6324cd82e03a882",
|
||||||
|
"source": "https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
771
assets/web_export/highlight.min.js
vendored
771
assets/web_export/highlight.min.js
vendored
File diff suppressed because one or more lines are too long
79
assets/web_export/marked.min.js
vendored
79
assets/web_export/marked.min.js
vendored
File diff suppressed because one or more lines are too long
3
assets/web_export/purify.min.js
vendored
Normal file
3
assets/web_export/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,7 @@ import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
|
||||||
import '../models/chart.dart';
|
import '../models/chart.dart';
|
||||||
import '../models/settings.dart';
|
import '../models/settings.dart';
|
||||||
|
import '../utils/log.dart';
|
||||||
|
|
||||||
/// Builds a single, self-contained HTML file from a deck's Marp Markdown.
|
/// Builds a single, self-contained HTML file from a deck's Marp Markdown.
|
||||||
///
|
///
|
||||||
|
|
@ -38,6 +39,7 @@ class MarpHtmlService {
|
||||||
/// colours and font so the export matches the in-app / PDF look.
|
/// colours and font so the export matches the in-app / PDF look.
|
||||||
Future<String> build(String deckMarkdown, {ThemeProfile? theme}) async {
|
Future<String> build(String deckMarkdown, {ThemeProfile? theme}) async {
|
||||||
final marked = await loadAsset('$_assetDir/marked.min.js');
|
final marked = await loadAsset('$_assetDir/marked.min.js');
|
||||||
|
final purify = await loadAsset('$_assetDir/purify.min.js');
|
||||||
final hljs = await loadAsset('$_assetDir/highlight.min.js');
|
final hljs = await loadAsset('$_assetDir/highlight.min.js');
|
||||||
final hljsCss = await loadAsset('$_assetDir/highlight.css');
|
final hljsCss = await loadAsset('$_assetDir/highlight.css');
|
||||||
final mathjax = await loadAsset('$_assetDir/tex-svg.js');
|
final mathjax = await loadAsset('$_assetDir/tex-svg.js');
|
||||||
|
|
@ -61,6 +63,7 @@ class MarpHtmlService {
|
||||||
'<style>$css\n$hljsCss</style>'
|
'<style>$css\n$hljsCss</style>'
|
||||||
'<script>$_mathjaxConfig</script>'
|
'<script>$_mathjaxConfig</script>'
|
||||||
'${inline(marked)}'
|
'${inline(marked)}'
|
||||||
|
'${inline(purify)}'
|
||||||
'${inline(hljs)}'
|
'${inline(hljs)}'
|
||||||
'${inline(mathjax)}'
|
'${inline(mathjax)}'
|
||||||
'${inline(mermaid)}'
|
'${inline(mermaid)}'
|
||||||
|
|
@ -97,11 +100,16 @@ class MarpHtmlService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Neutralise any `</script` inside inlined content so it can't break out of
|
/// Neutralise any `</script` inside inlined content so it can't break out of
|
||||||
/// the surrounding <script> element. Safe for both JS (string contexts) and
|
/// the surrounding <script> element. Case-insensitive — `</ScRiPt>` must not
|
||||||
/// the embedded Markdown payloads.
|
/// slip through. Safe for both JS (string contexts) and the embedded Markdown
|
||||||
static String _guard(String s) => s
|
/// payloads.
|
||||||
.replaceAll('</script', r'<\/script')
|
static final RegExp _scriptClose = RegExp(
|
||||||
.replaceAll('</SCRIPT', r'<\/SCRIPT');
|
r'</(script)',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
static String _guard(String s) =>
|
||||||
|
s.replaceAllMapped(_scriptClose, (m) => '<\\/${m.group(1)}');
|
||||||
|
|
||||||
// ── Charts → inline SVG ────────────────────────────────────────────────────
|
// ── Charts → inline SVG ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -563,7 +571,9 @@ class MarpHtmlService {
|
||||||
final range = (rawHi - rawLo).abs();
|
final range = (rawHi - rawLo).abs();
|
||||||
final r = range <= 0 ? 1.0 : range;
|
final r = range <= 0 ? 1.0 : range;
|
||||||
final rawStep = r / 4;
|
final rawStep = r / 4;
|
||||||
final mag = math.pow(10, (math.log(rawStep) / math.ln10).floor()).toDouble();
|
final mag = math
|
||||||
|
.pow(10, (math.log(rawStep) / math.ln10).floor())
|
||||||
|
.toDouble();
|
||||||
final norm = rawStep / mag;
|
final norm = rawStep / mag;
|
||||||
final niceNorm = norm < 1.5
|
final niceNorm = norm < 1.5
|
||||||
? 1.0
|
? 1.0
|
||||||
|
|
@ -640,7 +650,8 @@ class MarpHtmlService {
|
||||||
return "@font-face{font-family:'EB Garamond';font-weight:400 800;"
|
return "@font-face{font-family:'EB Garamond';font-weight:400 800;"
|
||||||
"font-style:normal;src:url(data:font/ttf;base64,$b64) "
|
"font-style:normal;src:url(data:font/ttf;base64,$b64) "
|
||||||
"format('truetype');}";
|
"format('truetype');}";
|
||||||
} catch (_) {
|
} catch (e) {
|
||||||
|
logWarning('MarpHtmlService._ebGaramondFontFace: load font asset', e);
|
||||||
return ''; // Fall back to the CSS font stack if the asset is missing.
|
return ''; // Fall back to the CSS font stack if the asset is missing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -672,7 +683,11 @@ body{background:#1e1e1e;font-family:-apple-system,"Segoe UI",Roboto,Helvetica,Ar
|
||||||
var holder=sec.querySelector('script[type="text/markdown"]');
|
var holder=sec.querySelector('script[type="text/markdown"]');
|
||||||
var src=holder?holder.textContent:'';
|
var src=holder?holder.textContent:'';
|
||||||
var div=document.createElement('div');div.className='content';
|
var div=document.createElement('div');div.className='content';
|
||||||
div.innerHTML=window.marked?marked.parse(src):src;
|
var html=window.marked?marked.parse(src):src;
|
||||||
|
// Sanitise rendered Markdown before it touches the DOM: a deck must not be
|
||||||
|
// able to run script/onerror/javascript: payloads when the export is opened.
|
||||||
|
// If the sanitiser somehow isn't present, fail closed to plain text.
|
||||||
|
if(window.DOMPurify){div.innerHTML=DOMPurify.sanitize(html);}else{div.textContent=src;}
|
||||||
sec.innerHTML='';sec.appendChild(div);
|
sec.innerHTML='';sec.appendChild(div);
|
||||||
});
|
});
|
||||||
document.querySelectorAll('code.language-mermaid').forEach(function(code){
|
document.querySelectorAll('code.language-mermaid').forEach(function(code){
|
||||||
|
|
|
||||||
178
tool/check_bundled_js.dart
Normal file
178
tool/check_bundled_js.dart
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
// Security gate for the vendored JavaScript bundles that get inlined into the
|
||||||
|
// offline HTML export (marked, highlight.js, DOMPurify, mermaid, MathJax — see
|
||||||
|
// lib/services/marp_html_service.dart). Unlike Dart packages, these are checked
|
||||||
|
// into the repo by hand and are not covered by `flutter pub outdated`, so this
|
||||||
|
// tool is their dedicated safety net.
|
||||||
|
//
|
||||||
|
// dart run tool/check_bundled_js.dart (or: make deps-check)
|
||||||
|
//
|
||||||
|
// It does two independent things, both of which can fail the build:
|
||||||
|
//
|
||||||
|
// 1. Integrity (offline, deterministic): every file listed in
|
||||||
|
// assets/web_export/MANIFEST.json must still hash to the recorded sha256.
|
||||||
|
// Catches a bundle that was swapped, truncated, or edited without the
|
||||||
|
// manifest (and therefore this review) being updated.
|
||||||
|
//
|
||||||
|
// 2. Known vulnerabilities (online): each pinned package@version is queried
|
||||||
|
// against the OSV database (https://osv.dev). Any advisory fails the gate
|
||||||
|
// so we learn a vendored bundle needs upgrading the moment a CVE lands —
|
||||||
|
// not at the next manual audit.
|
||||||
|
//
|
||||||
|
// Exit codes: 0 = clean 1 = integrity mismatch or known vulnerability
|
||||||
|
// 2 = could not run the check (missing manifest / no network)
|
||||||
|
//
|
||||||
|
// Run it routinely (it is wired into `make deps-check` and CI). When it flags a
|
||||||
|
// vulnerability, upgrade the bundle, refresh its version + sha256 in
|
||||||
|
// MANIFEST.json, and update THIRD_PARTY_NOTICES.md in the same commit.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
|
||||||
|
const _manifestPath = 'assets/web_export/MANIFEST.json';
|
||||||
|
const _assetDir = 'assets/web_export';
|
||||||
|
const _osvUrl = 'https://api.osv.dev/v1/query';
|
||||||
|
|
||||||
|
Future<void> main(List<String> args) async {
|
||||||
|
final offline = args.contains('--offline');
|
||||||
|
|
||||||
|
final manifestFile = File(_manifestPath);
|
||||||
|
if (!manifestFile.existsSync()) {
|
||||||
|
stderr.writeln('No $_manifestPath — cannot check the bundled JS.');
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
final manifest =
|
||||||
|
jsonDecode(manifestFile.readAsStringSync()) as Map<String, dynamic>;
|
||||||
|
final ecosystem = (manifest['ecosystem'] as String?) ?? 'npm';
|
||||||
|
final bundles = (manifest['bundles'] as List).cast<Map<String, dynamic>>();
|
||||||
|
|
||||||
|
stdout.writeln('== OciDeck check: bundled JavaScript ==');
|
||||||
|
stdout.writeln('Manifest: $_manifestPath (${bundles.length} bundles)\n');
|
||||||
|
|
||||||
|
final integrityProblems = <String>[];
|
||||||
|
final vulnProblems = <String>[];
|
||||||
|
var networkFailed = false;
|
||||||
|
|
||||||
|
for (final b in bundles) {
|
||||||
|
final file = b['file'] as String;
|
||||||
|
final npm = b['npm'] as String;
|
||||||
|
final version = b['version'] as String;
|
||||||
|
final expected = (b['sha256'] as String).toLowerCase();
|
||||||
|
final path = '$_assetDir/$file';
|
||||||
|
|
||||||
|
// --- 1. Integrity -------------------------------------------------------
|
||||||
|
final f = File(path);
|
||||||
|
String integrity;
|
||||||
|
if (!f.existsSync()) {
|
||||||
|
integrity = 'MISSING FILE';
|
||||||
|
integrityProblems.add('$file — file not found at $path');
|
||||||
|
} else {
|
||||||
|
final actual = sha256.convert(f.readAsBytesSync()).toString();
|
||||||
|
if (actual == expected) {
|
||||||
|
integrity = 'sha256 ok';
|
||||||
|
} else {
|
||||||
|
integrity = 'SHA256 MISMATCH';
|
||||||
|
integrityProblems.add(
|
||||||
|
'$file — sha256 differs from manifest\n'
|
||||||
|
' expected $expected\n'
|
||||||
|
' actual $actual',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Known vulnerabilities (OSV) ------------------------------------
|
||||||
|
String vulnStatus;
|
||||||
|
if (offline) {
|
||||||
|
vulnStatus = 'skipped (--offline)';
|
||||||
|
} else {
|
||||||
|
final r = await _queryOsv(ecosystem, npm, version);
|
||||||
|
if (r == null) {
|
||||||
|
vulnStatus = 'OSV UNREACHABLE';
|
||||||
|
networkFailed = true;
|
||||||
|
} else if (r.isEmpty) {
|
||||||
|
vulnStatus = 'no known CVEs';
|
||||||
|
} else {
|
||||||
|
vulnStatus = '${r.length} ADVISORY(IES): ${r.join(', ')}';
|
||||||
|
vulnProblems.add('$npm@$version → ${r.join(', ')}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.writeln(' $npm@$version ($file)');
|
||||||
|
stdout.writeln(' integrity : $integrity');
|
||||||
|
stdout.writeln(' osv : $vulnStatus');
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.writeln('');
|
||||||
|
|
||||||
|
if (integrityProblems.isEmpty && vulnProblems.isEmpty && !networkFailed) {
|
||||||
|
stdout.writeln(
|
||||||
|
'OK — all bundles match the manifest and have no known CVEs.',
|
||||||
|
);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integrityProblems.isNotEmpty) {
|
||||||
|
stderr.writeln('INTEGRITY — ${integrityProblems.length} problem(s):');
|
||||||
|
for (final p in integrityProblems) {
|
||||||
|
stderr.writeln(' $p');
|
||||||
|
}
|
||||||
|
stderr.writeln(
|
||||||
|
' A mismatch means a bundle changed without MANIFEST.json being updated.\n'
|
||||||
|
' If the change was intentional, refresh the version + sha256 there.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vulnProblems.isNotEmpty) {
|
||||||
|
stderr.writeln('\nVULNERABILITIES — ${vulnProblems.length} bundle(s):');
|
||||||
|
for (final p in vulnProblems) {
|
||||||
|
stderr.writeln(' $p');
|
||||||
|
}
|
||||||
|
stderr.writeln(
|
||||||
|
' Upgrade the bundle, then update MANIFEST.json + THIRD_PARTY_NOTICES.md.\n'
|
||||||
|
' Advisory detail: https://osv.dev/<ID>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integrityProblems.isNotEmpty || vulnProblems.isNotEmpty) exit(1);
|
||||||
|
|
||||||
|
// Only network trouble, nothing actually wrong with the bundles.
|
||||||
|
stderr.writeln(
|
||||||
|
'COULD NOT VERIFY CVEs — OSV was unreachable. Integrity passed.\n'
|
||||||
|
' Re-run with network access, or `--offline` to check integrity only.',
|
||||||
|
);
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the list of OSV advisory IDs affecting [name]@[version], an empty
|
||||||
|
/// list if there are none, or null if the database could not be reached.
|
||||||
|
Future<List<String>?> _queryOsv(
|
||||||
|
String ecosystem,
|
||||||
|
String name,
|
||||||
|
String version,
|
||||||
|
) async {
|
||||||
|
final client = HttpClient()..connectionTimeout = const Duration(seconds: 15);
|
||||||
|
try {
|
||||||
|
final req = await client.postUrl(Uri.parse(_osvUrl));
|
||||||
|
req.headers.contentType = ContentType.json;
|
||||||
|
req.write(
|
||||||
|
jsonEncode({
|
||||||
|
'package': {'name': name, 'ecosystem': ecosystem},
|
||||||
|
'version': version,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
final resp = await req.close().timeout(const Duration(seconds: 20));
|
||||||
|
if (resp.statusCode != 200) return null;
|
||||||
|
final body = await resp.transform(utf8.decoder).join();
|
||||||
|
final data = jsonDecode(body) as Map<String, dynamic>;
|
||||||
|
final vulns = (data['vulns'] as List?) ?? const [];
|
||||||
|
return vulns
|
||||||
|
.map((v) => (v as Map<String, dynamic>)['id'] as String)
|
||||||
|
.toList();
|
||||||
|
} on Object {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
client.close(force: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue