Skip to content

Commit ed51dd6

Browse files
laffer1claude
andcommitted
Add MNBSD-2026-17 through 63; sync-github-advisories skill
Backfill 2026 advisories 17-63 from the GitHub security advisories at github.com/MidnightBSD/src/security/advisories, in OSV YAML format. Also add a skill that automates this sync procedure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 918233c commit ed51dd6

50 files changed

Lines changed: 1383 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
name: sync-github-advisories
3+
description: Sync/migrate MidnightBSD GitHub Security Advisories into OSV YAML files in vulns/midnightbsd/. Use when the local advisory list is behind the GitHub advisories at github.com/MidnightBSD/src/security/advisories, or when asked to import/catch up/backfill MNBSD-YYYY-* advisories.
4+
---
5+
6+
# Sync GitHub Security Advisories → OSV YAML
7+
8+
This skill imports published GitHub Security Advisories (GHSA) from the
9+
MidnightBSD source repo and writes matching OSV-format YAML advisories into
10+
`vulns/midnightbsd/`, keeping the local list in sync with GitHub. The website is
11+
published from these YAML files, so they must stay in sync.
12+
13+
## Data source
14+
15+
GitHub advisories live at
16+
`https://github.com/MidnightBSD/src/security/advisories` and are read via the
17+
REST API with the `gh` CLI:
18+
19+
```bash
20+
gh api /repos/MidnightBSD/src/security-advisories --paginate
21+
```
22+
23+
Each advisory's `summary` starts with its MNBSD identifier, e.g.
24+
`"MNBSD-2026-63 Multiple vulnerabilities in OpenZFS ..."`. That prefix is how a
25+
GHSA is mapped to an `MNBSD-YYYY-ID.yaml` file.
26+
27+
## Quick start
28+
29+
Run the bundled script from the repository root. It fetches all published
30+
advisories, writes YAML for any MNBSD id not already present, and bumps
31+
`latest-id.txt`:
32+
33+
```bash
34+
python3 .claude/skills/sync-github-advisories/sync_advisories.py
35+
```
36+
37+
Common variations:
38+
39+
```bash
40+
# Preview without writing
41+
python3 .claude/skills/sync-github-advisories/sync_advisories.py --dry-run
42+
43+
# Only a specific range / year
44+
python3 .claude/skills/sync-github-advisories/sync_advisories.py --year 2026 --from 17 --to 63
45+
46+
# Regenerate (overwrite) files that already exist
47+
python3 .claude/skills/sync-github-advisories/sync_advisories.py --from 40 --to 45 --force
48+
```
49+
50+
By default existing YAML files are **skipped** (never clobbered). Use `--force`
51+
only when you intend to regenerate them.
52+
53+
After writing YAML, regenerate the HTML:
54+
55+
```bash
56+
python3 scripts/osvtohtml.py vulns/midnightbsd
57+
```
58+
59+
## Field mapping (GHSA → OSV YAML)
60+
61+
The script encodes the following procedure. Understand it so you can hand-fix
62+
edge cases the script gets wrong.
63+
64+
| OSV YAML field | Source in the GitHub advisory |
65+
|-----------------------|-----------------------------------------------------------------------------------------------|
66+
| `id` | `MNBSD-YYYY-ID` parsed from the `summary` prefix |
67+
| `summary` | `summary` with the `MNBSD-YYYY-ID ` prefix stripped (JSON-quoted if it starts with `"` or has `: `) |
68+
| `details` | The advisory `description`, minus the Patches/References/Solution/Credits/Affected-Versions sections, with markdown headers, `**bold**`, and `` `backticks` `` stripped (it renders inside a single `<p>`) |
69+
| `affected[].package.name` | `vulnerabilities[].package.name` (kept verbatim, e.g. `kernel/posixshm`, `libc/iconv`, `openzfs`) |
70+
| `affected[].package.ecosystem` | Always `MidnightBSD` |
71+
| `affected[].ranges` (ECOSYSTEM) | `introduced` from `vulnerable_version_range` (`0`, unless it uses `>= X`); `fixed` = first version in `patched_versions` |
72+
| `references` | URLs in the description's References section; else `cve.org` records for each CVE; else the GHSA `html_url` |
73+
| `aliases` | **All** `CVE-…` ids found in the summary + description (not just `cve_id` — multi-CVE advisories list several) |
74+
| `modified` / `published` | `published_at` (falls back to `updated_at`/`created_at`) |
75+
76+
`latest-id.txt` is updated to the highest ID of the most recent year processed.
77+
78+
## Things to watch for
79+
80+
- **No CVE yet.** Some advisories (e.g. msearch) have no CVE assigned. The
81+
script writes no `aliases` and references the GHSA advisory URL instead. This
82+
is expected — don't invent a CVE.
83+
- **Multi-CVE advisories.** The GitHub `cve_id`/`identifiers` fields often list
84+
only the primary CVE; the script scrapes every `CVE-...` from the text so all
85+
are captured as aliases.
86+
- **Package names.** Newer advisories use qualified names like `kernel/posixshm`
87+
or `libc/iconv`. These are kept as-is. Collapse to `kernel` only if the
88+
maintainer asks.
89+
- **Verify before committing.** Confirm YAML parses and spot-check a simple and a
90+
multi-vuln file:
91+
```bash
92+
python3 -c "import yaml,glob; [yaml.safe_load(open(f)) for f in glob.glob('vulns/midnightbsd/*.yaml')]"
93+
```
94+
- Nothing is committed automatically — review the diff, then commit the new
95+
`.yaml`/`.html` files and `latest-id.txt`.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env python3
2+
"""Sync MidnightBSD GitHub Security Advisories into OSV YAML files.
3+
4+
Fetches published security advisories from a GitHub repository (default
5+
MidnightBSD/src) via the `gh` CLI, maps each one to its MNBSD-YYYY-ID identifier
6+
(parsed from the advisory summary), and writes an OSV-format YAML file for every
7+
advisory that is not already present in vulns/midnightbsd/.
8+
9+
Usage:
10+
python3 sync_advisories.py [options]
11+
12+
Options:
13+
--repo OWNER/NAME GitHub repo to read advisories from (default MidnightBSD/src)
14+
--out DIR Output directory (default vulns/midnightbsd relative to CWD)
15+
--latest-id FILE latest-id.txt path to update (default ./latest-id.txt)
16+
--year YYYY Only process MNBSD-YYYY-* advisories (default: all years)
17+
--from N Lowest numeric ID to (re)generate (default: all)
18+
--to M Highest numeric ID to (re)generate (default: all)
19+
--force Overwrite YAML files that already exist
20+
--dry-run Print what would be written without writing
21+
22+
After running, regenerate HTML with:
23+
python3 scripts/osvtohtml.py vulns/midnightbsd
24+
"""
25+
26+
import argparse, json, os, re, subprocess, sys
27+
28+
EXCLUDE = ('patch', 'reference', 'solution', 'credit', 'affected version')
29+
30+
31+
def fetch_advisories(repo):
32+
out = subprocess.check_output(
33+
['gh', 'api', '/repos/%s/security-advisories' % repo, '--paginate'])
34+
return json.loads(out)
35+
36+
37+
def split_sections(desc):
38+
lines = desc.replace('\r\n', '\n').replace('\r', '\n').split('\n')
39+
sections, head, body = [], None, []
40+
for ln in lines:
41+
hm = re.match(r'^\s*#{1,6}\s*(.+?)\s*$', ln)
42+
if hm:
43+
if head is not None or body:
44+
sections.append((head, body))
45+
head, body = hm.group(1), []
46+
else:
47+
body.append(ln)
48+
if head is not None or body:
49+
sections.append((head, body))
50+
return sections
51+
52+
53+
def clean_inline(t):
54+
t = t.replace('**', '')
55+
return re.sub(r'`([^`]*)`', r'\1', t)
56+
57+
58+
def build_details(desc):
59+
parts = []
60+
for head, body in split_sections(desc):
61+
if head and any(x in head.lower() for x in EXCLUDE):
62+
continue
63+
text = clean_inline('\n'.join(body)).strip()
64+
if text:
65+
parts.append(text)
66+
return re.sub(r'\n{3,}', '\n\n', '\n\n'.join(parts)).strip()
67+
68+
69+
def extract_refs(desc):
70+
urls = []
71+
for head, body in split_sections(desc):
72+
if head and 'reference' in head.lower():
73+
for ln in body:
74+
for u in re.findall(r'https?://\S+', ln):
75+
urls.append(u.rstrip('.,);'))
76+
return urls
77+
78+
79+
def parse_range(vr, patched):
80+
vr = (vr or '').strip()
81+
introduced = '0'
82+
m = re.match(r'>=\s*([0-9][\w.\-]*)', vr)
83+
if m:
84+
introduced = m.group(1)
85+
fixed = None
86+
if patched:
87+
fm = re.search(r'([0-9]+(?:\.[0-9]+)+)', patched)
88+
if fm:
89+
fixed = fm.group(1)
90+
return introduced, fixed
91+
92+
93+
def q(v):
94+
return '"%s"' % v
95+
96+
97+
def summary_line(summary):
98+
if re.search(r'^["\'\[\]{}#&*!|>%@`]|:\s|\s#', summary) or summary != summary.strip():
99+
return 'summary: %s' % json.dumps(summary)
100+
return 'summary: %s' % summary
101+
102+
103+
def render_yaml(aid, num, adv):
104+
summary = re.sub(r'^%s\s*' % re.escape(aid), '', adv['summary']).strip()
105+
details = build_details(adv['description'])
106+
107+
cves = []
108+
for c in re.findall(r'CVE-\d{4}-\d+', adv['summary'] + ' ' + adv['description']):
109+
if c not in cves:
110+
cves.append(c)
111+
112+
refs = extract_refs(adv['description'])
113+
if not refs:
114+
refs = (['https://www.cve.org/CVERecord?id=%s' % c for c in cves]
115+
if cves else [adv['html_url']])
116+
117+
date = adv.get('published_at') or adv.get('updated_at') or adv.get('created_at')
118+
119+
lines = ['id: %s' % aid, summary_line(summary), 'details: |']
120+
for dl in details.split('\n'):
121+
lines.append('' if dl == '' else ' ' + dl)
122+
123+
lines.append('affected:')
124+
for v in adv['vulnerabilities']:
125+
introduced, fixed = parse_range(v.get('vulnerable_version_range', ''),
126+
v.get('patched_versions', ''))
127+
lines += [' - package:',
128+
' name: %s' % v['package']['name'],
129+
' ecosystem: MidnightBSD',
130+
' ranges:',
131+
' - type: ECOSYSTEM',
132+
' events:',
133+
' - introduced: %s' % q(introduced)]
134+
if fixed:
135+
lines.append(' - fixed: %s' % q(fixed))
136+
137+
lines.append('references:')
138+
for u in refs:
139+
lines += [' - type: WEB', ' url: %s' % u]
140+
141+
if cves:
142+
lines.append('aliases:')
143+
lines += [' - %s' % c for c in cves]
144+
145+
lines += ['modified: %s' % q(date), 'published: %s' % q(date)]
146+
return '\n'.join(lines) + '\n'
147+
148+
149+
def main():
150+
ap = argparse.ArgumentParser()
151+
ap.add_argument('--repo', default='MidnightBSD/src')
152+
ap.add_argument('--out', default='vulns/midnightbsd')
153+
ap.add_argument('--latest-id', default='latest-id.txt')
154+
ap.add_argument('--year', type=int)
155+
ap.add_argument('--from', dest='lo', type=int)
156+
ap.add_argument('--to', dest='hi', type=int)
157+
ap.add_argument('--force', action='store_true')
158+
ap.add_argument('--dry-run', action='store_true')
159+
args = ap.parse_args()
160+
161+
data = fetch_advisories(args.repo)
162+
163+
parsed = {} # (year, num) -> adv
164+
for a in data:
165+
if a.get('state') != 'published':
166+
continue
167+
m = re.search(r'MNBSD-(\d{4})-(\d+)', a.get('summary', ''))
168+
if m:
169+
parsed[(int(m.group(1)), int(m.group(2)))] = a
170+
171+
written, skipped = [], []
172+
max_by_year = {}
173+
for (year, num), adv in sorted(parsed.items()):
174+
if args.year and year != args.year:
175+
continue
176+
if args.lo is not None and num < args.lo:
177+
continue
178+
if args.hi is not None and num > args.hi:
179+
continue
180+
aid = 'MNBSD-%d-%d' % (year, num)
181+
path = os.path.join(args.out, '%s.yaml' % aid)
182+
max_by_year[year] = max(max_by_year.get(year, 0), num)
183+
if os.path.exists(path) and not args.force:
184+
skipped.append(aid)
185+
continue
186+
content = render_yaml(aid, num, adv)
187+
if args.dry_run:
188+
print('--- would write %s ---' % path)
189+
print(content)
190+
else:
191+
with open(path, 'w') as f:
192+
f.write(content)
193+
written.append(aid)
194+
195+
print('Wrote %d file(s): %s' % (len(written), ', '.join(written) or '(none)'))
196+
if skipped:
197+
print('Skipped %d existing (use --force to overwrite): %s'
198+
% (len(skipped), ', '.join(skipped)))
199+
200+
# Update latest-id.txt to the highest ID for the most recent year seen.
201+
if max_by_year and not args.dry_run:
202+
year = max(max_by_year)
203+
newest = '%d-%d' % (year, max_by_year[year])
204+
cur = ''
205+
if os.path.exists(args.latest_id):
206+
cur = open(args.latest_id).read().strip()
207+
if cur != newest:
208+
with open(args.latest_id, 'w') as f:
209+
f.write(newest + '\n')
210+
print('Updated %s: %s -> %s' % (args.latest_id, cur or '(empty)', newest))
211+
212+
if written and not args.dry_run:
213+
print('\nNext: python3 scripts/osvtohtml.py %s' % args.out)
214+
215+
216+
if __name__ == '__main__':
217+
sys.exit(main())

latest-id.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2026-16
1+
2026-63
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
id: MNBSD-2026-17
2+
summary: out-of-bounds read/write in pcap_ether_aton via a malformed MAC address
3+
details: |
4+
pcap_ether_aton() in libpcap used an unbounded loop that parsed a caller-supplied MAC-48 address string without validating its format before allocation, allowing an out-of-bounds read and write when a malformed address string is supplied. Fixed by backporting the libpcap 1.10.6 input validation (upstream commit b2d2f9a9).
5+
affected:
6+
- package:
7+
name: libpcap
8+
ecosystem: MidnightBSD
9+
ranges:
10+
- type: ECOSYSTEM
11+
events:
12+
- introduced: "0"
13+
- fixed: "4.0.6"
14+
references:
15+
- type: WEB
16+
url: https://nvd.nist.gov/vuln/detail/CVE-2025-11961
17+
- type: WEB
18+
url: https://github.com/the-tcpdump-group/libpcap
19+
aliases:
20+
- CVE-2025-11961
21+
modified: "2026-06-11T03:08:10Z"
22+
published: "2026-06-11T03:08:10Z"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
id: MNBSD-2026-18
2+
summary: stack-based buffer overflow in postprocess_termcap via a crafted termcap ko capability
3+
details: |
4+
The ko capability processing loop in postprocess_termcap() copied termcap string values into a fixed buf2[MAX_TERMINFO_LENGTH] stack buffer without bounds checking, allowing a stack-based buffer overflow via a crafted termcap entry. Fixed by backporting the buffer-limit check from ncurses 6.5-20250329.
5+
affected:
6+
- package:
7+
name: ncurses
8+
ecosystem: MidnightBSD
9+
ranges:
10+
- type: ECOSYSTEM
11+
events:
12+
- introduced: "0"
13+
- fixed: "4.0.6"
14+
references:
15+
- type: WEB
16+
url: https://nvd.nist.gov/vuln/detail/CVE-2025-6141
17+
- type: WEB
18+
url: https://invisible-island.net/ncurses/
19+
aliases:
20+
- CVE-2025-6141
21+
modified: "2026-06-11T03:10:02Z"
22+
published: "2026-06-11T03:10:02Z"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
id: MNBSD-2026-19
2+
summary: C stack overflow in lua_resume via a maliciously crafted script
3+
details: |
4+
lua_resume() did not check for C stack overflow before resuming a coroutine, allowing a stack overflow to be triggered by a maliciously crafted Lua script. Fixed by updating contrib/lua to 5.4.7, which adds the missing C stack check in ldo.c.
5+
affected:
6+
- package:
7+
name: lua
8+
ecosystem: MidnightBSD
9+
ranges:
10+
- type: ECOSYSTEM
11+
events:
12+
- introduced: "0"
13+
- fixed: "4.0.6"
14+
references:
15+
- type: WEB
16+
url: https://nvd.nist.gov/vuln/detail/CVE-2021-43519
17+
- type: WEB
18+
url: https://www.lua.org/bugs.html
19+
aliases:
20+
- CVE-2021-43519
21+
modified: "2026-06-11T03:26:55Z"
22+
published: "2026-06-11T03:26:55Z"

0 commit comments

Comments
 (0)