Skip to content

Commit 851671a

Browse files
clay-goodclaude
andcommitted
fix(rules): BAA-027 detect 'agrees to indemnify' & 'defend, indemnify and hold harmless' covered-entity indemnity forms (v1.1.0)
v1.0.0 required "covered entity (shall|will) indemnif" with the verb sitting directly against the subject, catching only the barest form. It missed the three phrasings real indemnity clauses use: the "agrees to indemnify" verb, and the standard tripartite "shall defend, indemnify and hold harmless" / "shall defend and indemnify" where the "defend," / "defend and" clause breaks the adjacency. Split into a covered-entity branch (preserving the original hookless breadth) and a customer/client branch (retaining the HIPAA/BA hook), each broadened with the new verb set, an intervening defend/reimburse clause, and a bare "hold harmless" grant. exclude_if keeps the compliant negated / carved-out forms silent. Probe: 5 fire cases (incl. 3 previously-missed phrasings), 4 silent cases (each-party-responsible, negated, no-obligation, reversed BA-indemnity). Finding-delta over the contracts corpus and the v3/v4 goldens shows zero fired-flips and zero finding-count changes — the broadening adds no FPs. Mirrors the DPA-048 v1.1.0 fix for the GDPR controller-indemnity twin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 07df462 commit 851671a

342 files changed

Lines changed: 387 additions & 342 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/engine/rules/v3/baa/baa-ruleset.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,33 @@ describe("BAA-025 — HIPAA-remedy-impairing cap recognizes 'capped at / limited
487487
expect(await fires(b)).toBe(false);
488488
});
489489
});
490+
491+
describe("BAA-027 — CE-indemnity recognizes 'agrees to' / defend-indemnify-hold-harmless forms (v1.1.0)", () => {
492+
const fires = async (b: string) =>
493+
(
494+
await runEngine({
495+
rules: BAA_RULES,
496+
ctx: withBaa(buildContext(["BAA", b])),
497+
source_file: SRC,
498+
})
499+
).findings.some((f) => f.rule_id === "BAA-027");
500+
501+
it.each([
502+
"Covered Entity shall indemnify Business Associate for any HIPAA violations.",
503+
"Covered Entity agrees to indemnify Business Associate for any civil money penalties under HIPAA.",
504+
"Covered Entity shall defend, indemnify and hold harmless Business Associate against all HIPAA claims.",
505+
"Client agrees to indemnify and hold harmless Business Associate for any HIPAA penalties.",
506+
"Covered Entity shall defend and indemnify Business Associate from and against all HIPAA fines.",
507+
])("fires on CE/customer indemnity of the BA: %s", async (b) => {
508+
expect(await fires(b)).toBe(true);
509+
});
510+
511+
it.each([
512+
"Each party shall be responsible for its own violations of HIPAA.",
513+
"Covered Entity shall not indemnify Business Associate for the Business Associate's own HIPAA violations.",
514+
"Covered Entity shall have no obligation to indemnify Business Associate for HIPAA penalties.",
515+
"Business Associate shall indemnify Covered Entity for any HIPAA violations caused by Business Associate.",
516+
])("stays silent on the compliant / negated / reversed form: %s", async (b) => {
517+
expect(await fires(b)).toBe(false);
518+
});
519+
});

src/engine/rules/v3/baa/rules.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ export const BAA_RULES: Rule[] = [
552552

553553
language({
554554
id: "BAA-027",
555+
version: "1.1.0",
555556
name: "Covered entity indemnifies BA for HIPAA violations",
556557
description:
557558
"Flags clauses where the covered entity indemnifies the business associate for HIPAA violations — a common vendor overreach.",
@@ -563,9 +564,23 @@ export const BAA_RULES: Rule[] = [
563564
"HHS-frowned-upon drafting: shifting HIPAA liability from BA to the covered entity inverts the regulatory burden. The covered entity should not indemnify the BA for the BA's own HIPAA violations.",
564565
recommendation:
565566
"Restrict mutual indemnification to non-HIPAA matters, or remove the CE-to-BA indemnification entirely with respect to HIPAA breaches.",
567+
// v1.0.0 required "covered entity (shall|will) indemnif" with the verb
568+
// sitting DIRECTLY against the subject, so it caught only the barest form
569+
// and missed the three phrasings real indemnity clauses use: the "agrees to
570+
// indemnify" verb, and the standard tripartite "shall defend, indemnify and
571+
// hold harmless" / "shall defend and indemnify" where the "defend," /
572+
// "defend and" clause breaks the adjacency. Broaden the verb set, allow an
573+
// intervening defend/reimburse clause, accept a bare "hold harmless" grant,
574+
// and require the HIPAA hook so an ordinary non-HIPAA indemnity is not
575+
// flagged. exclude_if keeps the compliant negated / carved-out forms silent.
566576
bad_patterns: [
567-
/covered\s+entity\s+(shall|will)\s+indemnif/i,
568-
/(customer|client)\s+(shall|will)\s+indemnif.*?HIPAA/i,
577+
/covered\s+entity\s+(?:shall|will|must|agrees?\s+to)\s+(?:(?:defend|reimburse)\b[^.]{0,30})?(?:indemnif|hold\s+harmless)/i,
578+
/(?:customer|client)\s+(?:shall|will|must|agrees?\s+to)\s+(?:(?:defend|reimburse)\b[^.]{0,30})?(?:indemnif|hold\s+harmless)[^.]{0,120}(?:HIPAA|PHI|civil\s+money\s+penalt|business\s+associate)/is,
579+
],
580+
exclude_if: [
581+
/\bnot\s+(?:be\s+)?(?:obligated|required|liable)\s+to\s+(?:defend|indemnif|hold)/i,
582+
/\bno\s+(?:obligation|duty|liability)\s+to\s+(?:defend|indemnif|hold)/i,
583+
/(?:shall|will|does|do|agrees?)\s+not\s+(?:defend|indemnif|hold)/i,
569584
],
570585
}),
571586

tests/golden/v3/expected/ai-addendum-minimal-pass.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/golden/v3/expected/ai-addendum-missing-ai-definitions-fail.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/golden/v3/expected/ai-addendum-missing-fine-tuning-deletion-fail.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/golden/v3/expected/ai-addendum-missing-hallucination-and-human-review-fail.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/golden/v3/expected/ai-addendum-missing-output-ownership-fail.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/golden/v3/expected/ai-addendum-missing-subprocessor-disclosure-fail.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/golden/v3/expected/ai-addendum-missing-transparency-disclosures-fail.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/golden/v3/expected/ai-addendum-training-without-optin-fail.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)