Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ jobs:
# quinn endpoint. We only use reqwest as an HTTP/1.1+2 client and never
# accept inbound QUIC connections, so the vulnerable reassembly path is
# unreachable. Remove once reqwest pulls quinn-proto >= 0.11.15.
#
# quick-xml < 0.41 (two transitive copies: 0.37.5 via object_store,
# 0.38.4 via octofhir-ucum). Both RUSTSEC-2026-0194 (O(N^2) duplicate-
# attribute check) and RUSTSEC-2026-0195 (unbounded namespace-decl
# allocation) are DoS-via-malicious-XML fixed in 0.41.0. Neither copy
# parses attacker-controlled input:
# - octofhir-ucum uses quick-xml as a build-dependency only (parsing
# the bundled UCUM essence XML at build time); it is never linked
# into the shipped binary.
# - object_store parses XML responses from configured S3/Azure/GCS
# endpoints (trusted cloud infrastructure), not FHIR client input.
# helios-serde's own quick-xml (client FHIR XML) is already on 0.41 and
# sits behind the non-default `xml` feature. Remove these once
# object_store and octofhir-ucum ship builds on quick-xml >= 0.41.
run: >-
cargo audit
--ignore RUSTSEC-2026-0118
Expand All @@ -296,6 +310,8 @@ jobs:
--ignore RUSTSEC-2026-0099
--ignore RUSTSEC-2026-0104
--ignore RUSTSEC-2026-0185
--ignore RUSTSEC-2026-0194
--ignore RUSTSEC-2026-0195

coverage:
name: Code Coverage
Expand Down
133 changes: 130 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ default-members = [
"crates/hfs",
"crates/persistence",
"crates/rest",
"crates/web",
"crates/sof",
"crates/cds-hooks",
"crates/hts",
Expand Down
14 changes: 7 additions & 7 deletions crates/hts/src/backends/postgres/code_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,16 +564,16 @@ impl CodeSystemOperations for PostgresTerminologyBackend {
}

// Inactive concept: emit the canonical INACTIVE_CONCEPT_FOUND warning.
// The operations layer also appends a specific-status companion (e.g.
// "...status of retired...") via `lookup_concept_status`.
// When the concept's status is more specific than `inactive` (e.g.
// `retired`), the operations layer rewrites this issue's text in place
// to the merged form ("...status of retired and inactive...") via
// `lookup_concept_status`.
//
// No `location` field — the IG `validation/validate-contained-good`
// fixture (inline-VS path) pins this warning WITHOUT a location.
// The operations layer then clones the template for the
// specific-status companion, so the location-less template flows
// through both issues. URL-based VS-validate-code paths (e.g.
// `inactive-2a-validate`) emit their own INACTIVE_CONCEPT_FOUND
// WITH location via finish_validate_code_response, separately.
// URL-based VS-validate-code paths (e.g. `inactive-2a-validate`) emit
// their own INACTIVE_CONCEPT_FOUND WITH location via
// finish_validate_code_response, separately.
// Mirrors SQLite's CS-side behaviour (which doesn't emit at all).
if is_inactive {
issues.push(crate::types::ValidationIssue {
Expand Down
40 changes: 30 additions & 10 deletions crates/hts/src/backends/postgres/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,29 @@ pub async fn build_concept_closure_pg(
) -> Result<(), tokio_postgres::Error> {
use std::collections::{HashMap, VecDeque};

// Load all concept codes for this system.
let concepts: Vec<String> = client
// Do the whole build in ONE transaction that first pins the parent
// `code_systems` row with `FOR SHARE`. A concurrent `DELETE FROM
// code_systems` (e.g. `delete_normalized` removing a CodeSystem, which
// cascades to concepts/hierarchy/closure) then blocks until we commit and
// can't drop the row mid-build — which would otherwise make the closure
// INSERTs below violate `concept_closure_system_id_fkey`. If the row is
// already gone, another writer deleted the system first, so there is
// nothing to build.
let tx = client.transaction().await?;
if tx
.query_opt(
"SELECT 1 FROM code_systems WHERE id = $1 FOR SHARE",
&[&system_id],
)
.await?
.is_none()
{
tx.commit().await?;
return Ok(());
}

// Load all concept codes for this system (within the locked snapshot).
let concepts: Vec<String> = tx
.query(
"SELECT code FROM concepts WHERE system_id = $1",
&[&system_id],
Expand All @@ -296,12 +317,12 @@ pub async fn build_concept_closure_pg(
.collect();

if concepts.is_empty() {
client
.execute(
"DELETE FROM concept_closure WHERE system_id = $1",
&[&system_id],
)
.await?;
tx.execute(
"DELETE FROM concept_closure WHERE system_id = $1",
&[&system_id],
)
.await?;
tx.commit().await?;
return Ok(());
}

Expand All @@ -315,7 +336,7 @@ pub async fn build_concept_closure_pg(

// Build per-node children lists (index-based).
let mut children: Vec<Vec<usize>> = vec![Vec::new(); concepts.len()];
let rows = client
let rows = tx
.query(
"SELECT parent_code, child_code FROM concept_hierarchy WHERE system_id = $1",
&[&system_id],
Expand All @@ -336,7 +357,6 @@ pub async fn build_concept_closure_pg(
let mut anc_batch: Vec<&str> = Vec::with_capacity(BATCH);
let mut des_batch: Vec<&str> = Vec::with_capacity(BATCH);

let tx = client.transaction().await?;
tx.execute(
"DELETE FROM concept_closure WHERE system_id = $1",
&[&system_id],
Expand Down
Loading