nx_privacy_lib.nx source
↩ module page · 122 lines · 5578 B
1// nx_privacy_lib.nx -- PRIVACY COMPLIANCE: GDPR / CCPA subject-request clocks and breach notification.
2// The OneTrust-class category we had at zero. Two clocks decide almost all exposure here, and both are
3// routinely started from the wrong event.
4//
5// u2605THE 72-HOUR CLOCK RUNS FROM AWARENESS, NOT FROM THE BREACH (GDPR Art 33(1)). A controller that
6// discovers on day 30 an intrusion that began on day 1 has 72 hours from DAY 30. Teams anchor to the
7// incident date, conclude they are already hopelessly late, and then delay further -- or anchor to the
8// forensic-conclusion date and report far too late. Awareness is the anchor; the incident date is
9// evidence, not a deadline input.
10//
11// u2605AN EXTENSION MUST BE CLAIMED INSIDE THE ORIGINAL WINDOW. GDPR Art 12(3) allows a two-month extension
12// on a subject access request, and CCPA allows a further 45 days -- but only if the data subject is
13// informed WITHIN the original period. Notice given on day 31 of a 30-day window does not buy 60 more
14// days; it is simply a late response. Encoded so a late claim can never extend anything.
15//
16// u2605ENCRYPTION IS A DEFENCE TO SUBJECT NOTIFICATION, NOT TO REGULATOR NOTIFICATION (Art 34(3)(a) vs
17// Art 33). Rendering the data unintelligible excuses telling the individuals; it does NOT excuse telling
18// the supervisory authority. Conflating the two is a real and expensive error, so they are separate.
19//
20// All periods are DATA (rule 11). Time is in HOURS for the breach clock and DAYS for request clocks --
21// deliberately different units because the statutes use different units, and silently converting is how
22// 72 hours becomes "3 business days".
23// STRUCTURE: pure decision core, zero I/O. license_tier: ORIGINAL LIB.
24
25import "nx_matter_lib.nx"
26
27const PRV_UNSET: i64 = 0 - 2000000002
28const PRV_UNKNOWN: i64 = 0 - 1
29
30const PRV_ONTIME: i64 = 0
31const PRV_LATE: i64 = 1
32
33const PRV_GDPR_DSAR_DAYS: i64 = 30
34const PRV_GDPR_EXT_DAYS: i64 = 60
35const PRV_CCPA_DSAR_DAYS: i64 = 45
36const PRV_CCPA_EXT_DAYS: i64 = 45
37const PRV_BREACH_HOURS: i64 = 72
38
39func prv_is1(v: i64) -> i64 {
40 if v == 1 { return 1 }
41 return 0
42}
43
44// u2605THE EXTENSION RULE: an extension counts ONLY if notice of it was given within the original window.
45// base_days is the statutory period; ext_days the permitted extension.
46func prv_deadline_pure(received: i64, base_days: i64, ext_claimed: i64, ext_notice_day: i64, ext_days: i64) -> i64 {
47 if received == PRV_UNSET { return PRV_UNKNOWN }
48 let base: i64 = received + base_days
49 if prv_is1(ext_claimed) == 0 { return base }
50 if ext_notice_day == PRV_UNSET { return base }
51 if ext_notice_day > base { return base }
52 return base + ext_days
53}
54
55func prv_gdpr_dsar_pure(received: i64, ext_claimed: i64, ext_notice_day: i64) -> i64 {
56 return prv_deadline_pure(received, PRV_GDPR_DSAR_DAYS, ext_claimed, ext_notice_day, PRV_GDPR_EXT_DAYS)
57}
58
59func prv_ccpa_dsar_pure(received: i64, ext_claimed: i64, ext_notice_day: i64) -> i64 {
60 return prv_deadline_pure(received, PRV_CCPA_DSAR_DAYS, ext_claimed, ext_notice_day, PRV_CCPA_EXT_DAYS)
61}
62
63// responded on/before the deadline is ON TIME; unknown dates never resolve to on-time by default.
64func prv_response_status_pure(deadline: i64, responded: i64) -> i64 {
65 if deadline == PRV_UNKNOWN { return PRV_UNKNOWN }
66 if responded == PRV_UNSET { return PRV_UNKNOWN }
67 if responded <= deadline { return PRV_ONTIME }
68 return PRV_LATE
69}
70
71func prv_days_remaining_pure(deadline: i64, asof: i64) -> i64 {
72 if deadline == PRV_UNKNOWN { return PRV_UNSET }
73 if asof == PRV_UNSET { return PRV_UNSET }
74 return deadline - asof
75}
76
77// ---- breach notification ----
78
79// u2605the regulator deadline is 72 hours from AWARENESS. The incident time is deliberately NOT an input:
80// there is no way to accidentally anchor to it.
81func prv_breach_deadline_pure(aware_hour: i64) -> i64 {
82 if aware_hour == PRV_UNSET { return PRV_UNKNOWN }
83 return aware_hour + PRV_BREACH_HOURS
84}
85
86func prv_breach_status_pure(aware_hour: i64, reported_hour: i64) -> i64 {
87 let dl: i64 = prv_breach_deadline_pure(aware_hour)
88 if dl == PRV_UNKNOWN { return PRV_UNKNOWN }
89 if reported_hour == PRV_UNSET { return PRV_UNKNOWN }
90 if reported_hour <= dl { return PRV_ONTIME }
91 return PRV_LATE
92}
93
94// u2605Art 33: the SUPERVISORY AUTHORITY must be told about any personal-data breach that poses a risk.
95// Encryption does NOT excuse this.
96func prv_notify_authority_pure(is_personal_data: i64, risk_to_rights: i64) -> i64 {
97 if prv_is1(is_personal_data) == 0 { return 0 }
98 if prv_is1(risk_to_rights) == 0 { return 0 }
99 return 1
100}
101
102// u2605Art 34: DATA SUBJECTS must be told only on HIGH risk -- and rendering the data unintelligible
103// (encryption) is a defence HERE and only here.
104func prv_notify_subjects_pure(is_personal_data: i64, high_risk: i64, encrypted: i64) -> i64 {
105 if prv_is1(is_personal_data) == 0 { return 0 }
106 if prv_is1(high_risk) == 0 { return 0 }
107 if prv_is1(encrypted) == 1 { return 0 }
108 return 1
109}
110
111// total open exposure: late responses + unreported breaches past their window + unknown-status items.
112func prv_exposure_pure(late_responses: i64, late_breaches: i64, unknown: i64) -> i64 {
113 return late_responses + late_breaches + unknown
114}
115
116func prv_status_label(s: i64, out: *u8) -> i64 {
117 if s == PRV_ONTIME { mt_catcopy(out, 0, "ON-TIME" as *u8); out[7] = 0 as u8; return 7 }
118 if s == PRV_LATE { mt_catcopy(out, 0, "LATE" as *u8); out[4] = 0 as u8; return 4 }
119 mt_catcopy(out, 0, "UNKNOWN" as *u8)
120 out[7] = 0 as u8
121 return 7
122}