@@ -455,6 +455,7 @@ done | sort -u
455455| CVE details (severity, fixes) | ✅ | ❌ | — |
456456| Timeline navigation | ✅ | ❌ | — |
457457| SDK-first navigation | ✅ | ❌ | — |
458+ | Version diff (severity, products) | ✅ | ⚠️ Partial | 15x smaller |
458459
459460## Cache Coherency
460461
@@ -484,6 +485,236 @@ Result: Each file is a consistent snapshot
484485
485486The HAL ` _embedded ` pattern ensures that any data referenced within a document is included in that document. There are no "dangling pointers" to data that might not exist in a cached copy of another file.
486487
488+ ### Servicing Version Diff
489+
490+ #### Query: "What changed between .NET 8.0.15 and 8.0.22?"
491+
492+ | Schema | Files Required | Total Transfer |
493+ | --------| ----------------| ----------------|
494+ | hal-index | ` index.json ` → ` 8.0/index.json ` + 3 patch indexes | ** ~ 80 KB** |
495+ | releases-index | ` releases-index.json ` + ` 8.0/releases.json ` | ** 1,220 KB** (partial data) |
496+
497+ ** hal-index:**
498+
499+ This query requires two passes: first to get the release summaries, then to fetch CVE details for severity filtering and affected products.
500+
501+ ``` bash
502+ ROOT=" https://raw.githubusercontent.com/dotnet/core/release-index/release-notes/index.json"
503+
504+ # Configuration
505+ MAJOR_VERSION=" 8.0"
506+ FROM_PATCH=15
507+ TO_PATCH=22
508+ SEVERITY_FILTER=" CRITICAL" # Minimum severity: CRITICAL, HIGH, MEDIUM, or LOW (all)
509+
510+ # Step 1: Get the major version href
511+ VERSION_HREF=$( curl -s " $ROOT " | jq -r --arg ver " $MAJOR_VERSION " ' ._embedded.releases[] | select(.version == $ver) | ._links.self.href' )
512+
513+ # Step 2: Get release summaries and security release URLs
514+ VERSION_DATA=$( curl -s " $VERSION_HREF " )
515+
516+ # Extract security release URLs for CVE detail fetching
517+ SECURITY_HREFS=$( echo " $VERSION_DATA " | jq -r --argjson from " $FROM_PATCH " --argjson to " $TO_PATCH " '
518+ [._embedded.releases[] |
519+ select((.version | split(".")[2] | tonumber) > $from and (.version | split(".")[2] | tonumber) <= $to) |
520+ select(.security) |
521+ ._links.self.href] | .[]
522+ ' )
523+
524+ # Step 3: Fetch CVE details from each security release and aggregate
525+ CVE_DETAILS=$( for HREF in $SECURITY_HREFS ; do
526+ curl -s " $HREF " | jq -c ' ._embedded.disclosures[]? | {id, cvss_severity, affected_products, title}'
527+ done | jq -s ' unique_by(.id)' )
528+
529+ # Step 4: Generate the diff report with severity-filtered CVE IDs
530+ echo " $VERSION_DATA " | jq -r --arg major " $MAJOR_VERSION " --argjson from " $FROM_PATCH " --argjson to " $TO_PATCH " \
531+ --arg severity " $SEVERITY_FILTER " --argjson cve_details " $CVE_DETAILS " '
532+ # Filter releases in range (excluding start, including end)
533+ [._embedded.releases[] |
534+ select((.version | split(".")[2] | tonumber) > $from and (.version | split(".")[2] | tonumber) <= $to)
535+ ] as $releases |
536+
537+ # Filter CVEs by minimum severity
538+ [$cve_details[] | select(
539+ ($severity == "LOW") or
540+ ($severity == "MEDIUM" and (.cvss_severity == "MEDIUM" or .cvss_severity == "HIGH" or .cvss_severity == "CRITICAL")) or
541+ ($severity == "HIGH" and (.cvss_severity == "HIGH" or .cvss_severity == "CRITICAL")) or
542+ ($severity == "CRITICAL" and .cvss_severity == "CRITICAL")
543+ )] as $filtered_cves |
544+
545+ # Aggregate affected products across all CVEs
546+ [$cve_details[].affected_products // [] | .[]] | unique | sort as $all_products |
547+
548+ {
549+ from_version: "\($major).\($from)",
550+ to_version: "\($major).\($to)",
551+ from_date: (._embedded.releases[] | select(.version == "\($major).\($from)") | .date | split("T")[0]),
552+ to_date: (._embedded.releases[] | select(.version == "\($major).\($to)") | .date | split("T")[0]),
553+ total_releases: ($releases | length),
554+ security_releases: ([$releases[] | select(.security)] | length),
555+ non_security_releases: ([$releases[] | select(.security | not)] | length),
556+ total_cves: ([$releases[].cve_records? // [] | .[]] | unique | length),
557+ cve_ids: [$filtered_cves[] | .id],
558+ cve_severity_filter: $severity,
559+ affected_products: $all_products,
560+ releases: [$releases[] | {version, date: (.date | split("T")[0]), security, cve_count}]
561+ }
562+ '
563+ ```
564+
565+ ** Output:**
566+
567+ ``` json
568+ {
569+ "from_version" : " 8.0.15" ,
570+ "to_version" : " 8.0.22" ,
571+ "from_date" : " 2025-04-08" ,
572+ "to_date" : " 2025-11-11" ,
573+ "total_releases" : 7 ,
574+ "security_releases" : 3 ,
575+ "non_security_releases" : 4 ,
576+ "total_cves" : 5 ,
577+ "cve_ids" : [
578+ " CVE-2025-55315"
579+ ],
580+ "cve_severity_filter" : " CRITICAL" ,
581+ "affected_products" : [
582+ " aspnetcore-runtime" ,
583+ " dotnet-runtime" ,
584+ " windowsdesktop-runtime"
585+ ],
586+ "releases" : [
587+ { "version" : " 8.0.22" , "date" : " 2025-11-11" , "security" : false , "cve_count" : 0 },
588+ { "version" : " 8.0.21" , "date" : " 2025-10-14" , "security" : true , "cve_count" : 3 },
589+ { "version" : " 8.0.20" , "date" : " 2025-09-09" , "security" : false , "cve_count" : 0 },
590+ { "version" : " 8.0.19" , "date" : " 2025-08-05" , "security" : false , "cve_count" : 0 },
591+ { "version" : " 8.0.18" , "date" : " 2025-07-08" , "security" : false , "cve_count" : 0 },
592+ { "version" : " 8.0.17" , "date" : " 2025-06-10" , "security" : true , "cve_count" : 1 },
593+ { "version" : " 8.0.16" , "date" : " 2025-05-22" , "security" : true , "cve_count" : 1 }
594+ ]
595+ }
596+ ```
597+
598+ To include all CVEs regardless of severity:
599+
600+ ``` bash
601+ SEVERITY_FILTER=" LOW" # LOW is the minimum, so this includes all CVEs
602+ ```
603+
604+ The script above outputs ` from_date ` and ` to_date ` . To calculate the time gap, pipe the output through an additional jq filter:
605+
606+ ``` bash
607+ # Pipe the output to calculate days between versions
608+ ... | jq -r '
609+ # Parse ISO dates and calculate difference
610+ ((.to_date | strptime("%Y-%m-%d") | mktime) -
611+ (.from_date | strptime("%Y-%m-%d") | mktime)) / 86400 | floor as $days |
612+ . + {
613+ days_behind: $days,
614+ months_behind: (($days / 30) | floor)
615+ }
616+ '
617+ ```
618+
619+ This adds ` days_behind ` and ` months_behind ` to the output:
620+
621+ ``` json
622+ {
623+ "from_version" : " 8.0.15" ,
624+ "to_version" : " 8.0.22" ,
625+ "from_date" : " 2025-04-08" ,
626+ "to_date" : " 2025-11-11" ,
627+ "days_behind" : 217 ,
628+ "months_behind" : 7 ,
629+ ...
630+ }
631+ ```
632+
633+ ** releases-index:**
634+
635+ ``` bash
636+ ROOT=" https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json"
637+
638+ # Configuration
639+ MAJOR_VERSION=" 8.0"
640+ FROM_PATCH=15
641+ TO_PATCH=22
642+
643+ # Step 1: Get the releases.json URL for the major version
644+ RELEASES_URL=$( curl -s " $ROOT " | jq -r --arg ver " $MAJOR_VERSION " ' .["releases-index"][] | select(.["channel-version"] == $ver) | .["releases.json"]' )
645+
646+ # Step 2: Filter releases in range and aggregate
647+ curl -s " $RELEASES_URL " | jq -r --arg major " $MAJOR_VERSION " --argjson from " $FROM_PATCH " --argjson to " $TO_PATCH " '
648+ # Filter releases in range (excluding start, including end)
649+ [.releases[] | select(
650+ (.["release-version"] | split(".")[2] | tonumber) > $from and
651+ (.["release-version"] | split(".")[2] | tonumber) <= $to
652+ )] as $releases |
653+
654+ {
655+ from_version: "\($major).\($from)",
656+ to_version: "\($major).\($to)",
657+ from_date: ([.releases[] | select(.["release-version"] == "\($major).\($from)")] | .[0] | .["release-date"]),
658+ to_date: ([.releases[] | select(.["release-version"] == "\($major).\($to)")] | .[0] | .["release-date"]),
659+ total_releases: ($releases | length),
660+ security_releases: ([$releases[] | select(.security)] | length),
661+ non_security_releases: ([$releases[] | select(.security | not)] | length),
662+ total_cves: ([$releases[].["cve-list"]? // [] | .[]] | unique | length),
663+ cve_ids: ([$releases[].["cve-list"]? // [] | .[] | .["cve-id"]] | unique | sort),
664+ cve_severity_filter: "(not available)",
665+ affected_products: "(not available)",
666+ releases: [$releases[] | {
667+ version: .["release-version"],
668+ date: .["release-date"],
669+ security,
670+ cve_count: ([.["cve-list"]? // [] | .[]] | length)
671+ }]
672+ }
673+ '
674+ ```
675+
676+ ** Output:**
677+
678+ ``` json
679+ {
680+ "from_version" : " 8.0.15" ,
681+ "to_version" : " 8.0.22" ,
682+ "from_date" : " 2025-04-08" ,
683+ "to_date" : " 2025-11-11" ,
684+ "total_releases" : 7 ,
685+ "security_releases" : 3 ,
686+ "non_security_releases" : 4 ,
687+ "total_cves" : 5 ,
688+ "cve_ids" : [
689+ " CVE-2025-26646" ,
690+ " CVE-2025-30399" ,
691+ " CVE-2025-55247" ,
692+ " CVE-2025-55248" ,
693+ " CVE-2025-55315"
694+ ],
695+ "cve_severity_filter" : " (not available)" ,
696+ "affected_products" : " (not available)" ,
697+ "releases" : [
698+ { "version" : " 8.0.22" , "date" : " 2025-11-11" , "security" : false , "cve_count" : 0 },
699+ { "version" : " 8.0.21" , "date" : " 2025-10-14" , "security" : true , "cve_count" : 3 },
700+ { "version" : " 8.0.20" , "date" : " 2025-09-09" , "security" : false , "cve_count" : 0 },
701+ { "version" : " 8.0.19" , "date" : " 2025-08-05" , "security" : false , "cve_count" : 0 },
702+ { "version" : " 8.0.18" , "date" : " 2025-07-08" , "security" : false , "cve_count" : 0 },
703+ { "version" : " 8.0.17" , "date" : " 2025-06-10" , "security" : true , "cve_count" : 1 },
704+ { "version" : " 8.0.16" , "date" : " 2025-05-22" , "security" : true , "cve_count" : 1 }
705+ ]
706+ }
707+ ```
708+
709+ ** Analysis:**
710+
711+ - ** Completeness:** ⚠️ Partial—the releases-index can count releases and list CVE IDs, but cannot provide CVE severity, affected products, or detailed CVE information without fetching external CVE URLs.
712+ - ** Severity filtering:** The hal-index allows filtering ` cve_ids ` to specific severity levels (CRITICAL, HIGH, etc.) via the ` SEVERITY_FILTER ` variable, while ` total_cves ` always shows the complete count.
713+ - ** Affected products:** The hal-index aggregates all affected products across the version range (e.g., ` dotnet-runtime ` , ` aspnetcore-runtime ` ), enabling teams to identify which components need patching.
714+ - ** Executive reporting:** For CIO/CTO reporting, the hal-index provides actionable data (severity-filtered CVEs, affected products) while the releases-index only provides CVE IDs that require manual lookup.
715+
716+ ** Winner:** hal-index (** 15x smaller** , with CVE severity filtering and affected products)
717+
487718## Summary
488719
489720| Metric | hal-index | releases-index |
@@ -492,6 +723,7 @@ The HAL `_embedded` pattern ensures that any data referenced within a document i
492723| CVE queries (latest security patch) | 52 KB | 1,220 KB |
493724| Recent CVEs (last 2 security releases) | 23 KB | 2.5 MB |
494725| CVEs in last 12 months | ~ 90 KB | 2.5 MB |
726+ | Version diff with severity + products | ~ 80 KB | 1,220 KB (partial—no severity/products) |
495727| Cache coherency | ✅ Atomic | ❌ TTL mismatch risk |
496728| Query syntax | snake_case (dot notation) | kebab-case (bracket notation) |
497729| Link traversal | ` ._links.self.href ` | ` .["releases.json"] ` |
0 commit comments