From 486a05ecf197f49ccf22cb8db424b6228a7bab52 Mon Sep 17 00:00:00 2001 From: Nick Anderson Date: Fri, 13 Feb 2026 16:22:52 -0600 Subject: [PATCH] Added cipher and keyslot inventory to inventory-fde Extended inventory-fde to report active dm-crypt cipher per volume and LUKS keyslot details (per-keyslot cipher and PBKDF algorithm). LUKS2 metadata is cached as JSON with a 24-hour TTL. Gracefully degrades when dmsetup or cryptsetup are absent. Tool paths are defined as variables for single-point configuration. Includes loopback test helper script and Mission Portal screenshot. --- cfbs.json | 4 +- inventory/inventory-fde/README.md | 32 ++- .../inventory-fde-mission-portal.png | Bin 0 -> 47983 bytes inventory/inventory-fde/inventory-fde.cf | 230 ++++++++++++++++-- .../inventory-fde/test-encrypted-volume.sh | 96 ++++++++ 5 files changed, 331 insertions(+), 31 deletions(-) create mode 100644 inventory/inventory-fde/inventory-fde-mission-portal.png create mode 100755 inventory/inventory-fde/test-encrypted-volume.sh diff --git a/cfbs.json b/cfbs.json index f8af42b..00c8366 100644 --- a/cfbs.json +++ b/cfbs.json @@ -149,8 +149,8 @@ "tags": ["inventory", "security"], "subdirectory": "inventory/inventory-fde", "steps": [ - "copy inventory-fde.cf services/cfbs/inventory-fde/", - "policy_files services/cfbs/inventory-fde/", + "copy inventory-fde.cf services/cfbs/modules/inventory-fde/inventory-fde.cf", + "policy_files services/cfbs/modules/inventory-fde/inventory-fde.cf", "bundles inventory_fde:main" ] }, diff --git a/inventory/inventory-fde/README.md b/inventory/inventory-fde/README.md index e9d765a..f2c8a82 100644 --- a/inventory/inventory-fde/README.md +++ b/inventory/inventory-fde/README.md @@ -1,7 +1,8 @@ Full disk encryption (FDE) protects data at rest by encrypting entire block devices. This module detects mounted volumes backed by dm-crypt (LUKS1, LUKS2, or plain dm-crypt) on Linux systems and reports whether all, some, or none of the non-virtual block device filesystems are encrypted. -Detection is performed entirely through virtual filesystem reads (`/sys/block/` and `/proc/mounts`), with no dependency on external commands like `dmsetup` or `findmnt`. +Basic detection (encryption status, method, volume lists) is performed entirely through virtual filesystem reads (`/sys/block/` and `/proc/mounts`). +When `dmsetup` and `cryptsetup` are available, the module additionally reports the active cipher and LUKS keyslot details (per-keyslot cipher and PBKDF algorithm). ## How it works @@ -10,13 +11,19 @@ Detection is performed entirely through virtual filesystem reads (`/sys/block/` 3. Identifies crypt devices by the `CRYPT-` prefix in the UUID 4. Parses `/proc/mounts` to find all non-virtual block device mounts (excluding loop devices) 5. Classifies each mount as encrypted or unencrypted by checking if its device matches a crypt device path +6. If `dmsetup` is available, reads the active cipher from `dmsetup table` for each crypt device +7. If `cryptsetup` is available, reads LUKS keyslot metadata (cipher and PBKDF per slot) via `cryptsetup luksDump` ## Inventory -- **Full disk encryption enabled** -- `yes` if all non-virtual block device filesystems are encrypted, `partial` if some are encrypted and some are not, `no` if none are encrypted. -- **Full disk encryption method** -- The encryption type(s) detected, e.g. `LUKS2`, `LUKS1`, `PLAIN`, or `none`. Multiple types are comma-separated if different methods are in use. -- **Full disk encryption volumes** -- List of mountpoints backed by encrypted devices. -- **Unencrypted volumes** -- List of mountpoints on non-virtual block devices that are not encrypted. +- **Full disk encryption enabled** - `yes` if all non-virtual block device filesystems are encrypted, `partial` if some are encrypted and some are not, `no` if none are encrypted. +- **Full disk encryption methods** - The encryption type(s) detected, e.g. `LUKS2`, `LUKS1`, `PLAIN`. Empty list when no encryption is found. +- **Full disk encryption volumes** - List of mountpoints backed by encrypted devices. +- **Unencrypted volumes** - List of mountpoints on non-virtual block devices that are not encrypted. +- **Full disk encryption volume ciphers** - The active dm-crypt cipher per volume, e.g. `/ : aes-xts-plain64`. Requires `dmsetup`. +- **Full disk encryption keyslot info** - LUKS keyslot cipher and PBKDF per volume, e.g. `/ : 0:aes-xts-plain64/argon2id`. Requires `cryptsetup`. Not available for plain dm-crypt (no keyslots). + +[![Inventory in Mission Portal](inventory-fde-mission-portal.png)](inventory-fde-mission-portal.png) ## Example @@ -26,11 +33,24 @@ A system with LUKS2-encrypted root but unencrypted `/boot` and `/boot/efi`: $ sudo cf-agent -Kf ./inventory-fde.cf --show-evaluated-vars=inventory_fde Variable name Variable value Meta tags Comment inventory_fde:main.fde_enabled partial source=promise,inventory,attribute_name=Full disk encryption enabled -inventory_fde:main.fde_method LUKS2 source=promise,inventory,attribute_name=Full disk encryption method +inventory_fde:main.fde_method {"LUKS2"} source=promise,inventory,attribute_name=Full disk encryption methods inventory_fde:main.fde_volumes {"/"} source=promise,inventory,attribute_name=Full disk encryption volumes inventory_fde:main.unencrypted_volumes {"/boot","/boot/efi"} source=promise,inventory,attribute_name=Unencrypted volumes +inventory_fde:main.fde_volume_cipher {"/ : aes-xts-plain64"} source=promise,inventory,attribute_name=Full disk encryption volume ciphers +inventory_fde:main.fde_keyslot_info {"/ : 0:aes-xts-plain64/argon2id"} source=promise,inventory,attribute_name=Full disk encryption keyslot info +``` + +## Testing + +A helper script is included to create and tear down a LUKS2 test volume on a loopback device: + +``` +sudo ./test-encrypted-volume.sh setup # Create and mount test volume +sudo cf-agent -KIf ./inventory-fde.cf --show-evaluated-vars=inventory_fde +sudo ./test-encrypted-volume.sh teardown # Clean up ``` ## Platform - Linux only (requires `/sys/block/` and `/proc/mounts`) +- Cipher and keyslot inventory requires `dmsetup` and/or `cryptsetup` (typically available on systems with dm-crypt) diff --git a/inventory/inventory-fde/inventory-fde-mission-portal.png b/inventory/inventory-fde/inventory-fde-mission-portal.png new file mode 100644 index 0000000000000000000000000000000000000000..89f8ebc7b2a3fa0e983898fb22c6772339a678a5 GIT binary patch literal 47983 zcmeFYbx>SO6gP+kf+n~_aQEOA2*D+|yM^HH?(S|$@Zj$5HW1tfw}FAdePHvp>b+$5 zpRcxR>-%b@t8Vqoz4zSi{vGLaPIpIsR+K_VAwq$HfkBs%mQaC#LHvEe%D+bZeW!+# zBK&uS>>#b}3MnX)@!vG9)cfnDA89u)S7h@(RAO^yG=luxj zyfrAIqG!Ch&>~|p={M-TDUJA1DxJ7qc)MP7t4IVgcR#q5;u)ni15t`XI{>5S?1Ij> z05I4C00rL~^uJfY#{8?ahR{F||N4(PTKMjh#Gf*N{)&O}uVP0p{rB6BAH|{~{wl;M zDt~AtB-TDB{6$N-9TtT1H#IL#;a{{7E#LnqHZ62v_x7*8+`sgS^_`P4Yd1vAdn-EK zh{N%0yo(A+FE+TuYp~Y~>@)C;Z{-QI%CcCG{d-8fsld2NlfnLx$3EA(;hN{+Y64Nn z>+`s$9~khEAB}`JhwK9BOg-0KCylQd;+!X-o2VA~b(Z#=%w%_ADTQvwaaUhIqvao; zj4GJY@v{=jQM`12W8-1XiP#R}4nW!J^4Ry~$4?%J#rE`H;Qxx@9YwZR>FIt)yd=|_ z5ku2M4$1bj{R8&}>!b(hiMuwdEcQn=9Soh%JGhT_C? z!h!##(b91IOx3gk!8e|r<-2*oj?OEgCZqxw!jHp@ z2*gGf)DC8|0vgZ<_(+2{>~&2qt|bDzfFVHs`rde_``Pp%nOf`Uawru|=F z@-%TAsCT}>@ze9qL&KgJfIeOAkpLX8O&1RdTz6(`qREsG#_h?*#fOeJyJ(y z$}wk1mmjgmK~XaS)EkB6^!O7LY*j8lg8}WG$hFc&(n~)6DLp!V#v0LG5=bQ+i`FC0 zKY6r_s?`BS;Ajd;iLjA|7{oF_EI&Lbc6k{ymi*)`kQ}pHAdTr)rVn}`ZJ(}JG_FGl z;QaAywmxBAvFaa4T`r25h<^C1lVT6bylk6Ox)It`cAVLXofT&@68SP|7LRVkl`qmr zDCc0B=g|efRkCn-+%OO3OO4sWmxzqw{<7GGnMkqd@Ic5gK%IFP_Mo!a=7fV(@o%;LN>B{#fW=oX}ju>PSk&= zVALBKa!&B^_$gOi<}ZF6FY|Sqvts*c4=$+6c;Hq^kNZIhXIj48wD>nQ#CG^j@sgBn zL?X&$Yq!x6NjdQAToI4d6c3J&xVlZnJSI;{>7gQ#ap-tr}=4 zyxn`TaC2+WhsPM*vQF^io!5&pSbdel<_msSm_+SG3FO{Wlatnn_GBl0%HQ~0M)b%H zG?rUAZnWB&;AqHlpe3nDjNIPj#QcNhHugxY-z4?$a`SbY=2g#GrNl<~=~m-S5e`bG1F%Yo0Ghe7R#T^H!djgx)hwEgB{MJ8EoUc`!ee*5kOd&l7kGECH_&Ge?3ZnD zSsKa$CX?(}EjfG{e_0Ix8(5A5mA`T3o|26ULy560YMt$_isdi=R5(c`nQ}ie69%}e`zq@c*QKXX=n1<3Fyw~-#R*+ z+u$&0eib5LB+k=eXY1Q-$sz5H!%a{VY~)S<;KFZM-3Ym84 zPyH-F2Wi6fh=0y)y0X(lVfx&t@|qChH~dq*9fYZkzq&tf6Ml4FD8$mq`tS zAG)NLnz-WNQdd>y*-|^}d9krSr(^o+s;pp_>Y)vXIx=fFXY=4%8+O@Rb3{qpz1{2J z`9vve4uH259BJdGV`sd!(5fcFc^^Pjbar=vs!_2bu4Yz0;l}c*bfbur8$K5^(i4e$ zoMm%5P@hD;Ggy%Grsl**AoS~Ek%?Pb%AYCu347_y)K;j}!7dO@yWr9S>-eBz+eVse zNN6VXr@;r{YjBCdR*7dep{QZ^SN|K4k7^YFb*R zPR_Hliw>uECM=(Z11n{6>Jvu|S?J~)RBE)Sg%NMRS>4UE5auT)$anN7_l0)YIW>mT z8RQ{m(9)7g%#|rDg_+v`F%=r8ZNW0k5{+%R3?G@Oc~zts4#(}3}5|m=_Cnyt}7A*07`FpwAdn_gxkN zx!>TIDFU5B84{cl2V8;!zKLtPUc{wPo6NExgxu-PKl2W}{-)C@`*eORxuADeD4)J! zW`QlDX15YhW3PE}EFlvS&{j-S90@#qXa60Az$4XGq4`KtXGp}KHxHe_Z_Z9L zKGa*I&Sr}&3#cCZ5G-jlq1zL59eu~K+#}i{r10~>$Sl7jG=P)lY^gaYKc3bInN=a0 zA{T@S%GS13siB0DA1d`s6jkK%hMQT#U?!e;w&;GO6dEb}+8UCIGO4S2utaW?&mAl( z*Sr-e_*0dSGNY0>jnh$v6C?kFtord-C#jsl;HY3kcSh9YY`W0~ldltRuvnn%btzFz zcr0ORsV>XA_J^T*3F|QQYvbN+^opEL+-%KvH03=2GlWq~oWGgr zo|dU3)eHr;WIo9Yj0+48|)#%LnQGPs8m8mY@FGv1o zu}8Y&G(%52o^seWUOHl|HmYcWbSY!8E0~8MyWJ)6&x4#Ery1k;NUSC-dVzp9Z;Za! zc9#=a^SmzIh*xHbJ3GBVUCQZ=go06qL|Q3ts{)j3;DS%>rOL+T@!j};M?dLiT>fQO zxjxr@YKrd(dzpl}@Uy%1GeI&W{CNSGIvJdS&CW_MWO0|{_ZRgvX75|m!&g#UDn;e~ zNJXncq3!O8ch}bE_L4hr)x_X_bu>#>qo~j5UFfiMM+4d`_A^G6V2XJ365ab_HpG_G zXXs^S;Q(Et$@zD)0Lmae5)P7;ru0tT1&0NlC`H#OCW${^Pt+Z!3X!DB+lxzE)voj4 zw!X>Pm3N_xgA2q#W>d(un6%m`#4=vwEzsn7#c;|u6A{_G1A;`q;NmsiFV^N8U#`6Z zUo*>2RsqY1eLDH`{)0ARB(_GSRNmGLsGDuzY1k%X2c0wcW5y{dUzAE0#Q#KM692D_ zoc|XF?f;Ky+G7{!Koh4b`m1Q2xI%tvc^`PgeWP@$5Rq7~@Q(ZFT{bV+=yE~dX1y(% zWth^_|N6^{_a0F7FY&RHS|lkPnXjuo?|cM3Tx){?ENPoj8Lc;eWF0ecHM95z>#rOC zqjXHmy_oEZiDb&pD8ny>m(@h`Qub8AW;av!Nl1@Zg({%}At^TE^KS9yeY?T~0{9q? zB#(g@gZlB6vJT<)d>M@+5#<9#Yr17RRksX|w|XXe_!KgN<3}?2oJW8zwaS@*>9aNo zHHhS6qW$Ia4=U>YW}{K`D{}d5Kfi>8Y3V0%>*-N*UmO^z373Zu%SK|E&)L;W?xU^d z4sNr|>wZVZI^17AjV3j|W?9V{!J{JNx8Ov5LwC>)kR4j7UBgV9Z8F@FTx(`01^=qJ zf8t-6vlaiM<=PjnXgR1Wj65OIJ<-+laP_6C3(jbW>-se8uKiTE0hP^eY^1&9FHIJm znS7&iVJbQfQ9}E+e5Q2gcMq#LSe%rGHT8#GM$7Pvv(h z<`?UO&TohS23|6(=Crk^l}Lxz7xD|_u~hSRbS(3dp{JygS1!O`fVj|dK9CbX?8H*) zQxo$qnih_;7;tZbPAdl9ACb5pZhIvbHM*QX%>H$TBIyE^ctY(t=ZuqWB7XS2_@PMY ztKO>4v8CawjW>H$Y3w`N?|pHr_@f%tEW>ziU%i9&Orf1XQA8nfW6fUhFkHDi(ISPn z)5u`?@y8$K)`$MVhQ)D={*Hq z3%gK6M?KwjC+sSPh&Nam?^A0CT-Y0$qTvPC@NILdcM)NxEaKG!%9 z48nD?>k)I#F1_?e3-OO^ieC1hC&)GkD!pgr&=6TdeTx zvqg`$7F(b>msC&T_7T7;5H1uPR<>CEt^25Cp1f;5cyPMiJxMZ9hT|N}qjj<((E0<5 zh8dfRNs`+pO!&#?k`KQFnmD8FmV)K2FxwZ(Vu)YD5qCv^%v|4EDpr^#de{|HJg&5! zK%ir`iW3~HH9@DyLs~UrS}{xjOBk*x`8Ghw&8#ZXq_}qPScbXN9_@q^Rp+8*=y~yI zJi8Ibrs4@gKCzygqOV|zQvrz3e33(9J3yr4`U(s2tVJZ|YHFY}`Q- z3C?{c>(9T7)gJ}ql5kA;y+}2=`R54SPd1oayu~J$FQ+p!1()(jAnNwY?@g4M!J(sm z2MVaZ#M)L&c&T!w&Z911@_yqIL(DixDe>o}6xK%s+e&4MnDP?BSiE<(Y|)SdKeJF>1F_@wB0K z5*wDWgf|=ewPB&>a5Z>kC z{Y3s?Gal~ zV=FC#BXeeVYT-X0<`6E?@6Ub+ZgU=H$#|{!M`d>1oU$=uiE&QXq5%b(7uGRc>oxht zH*@?r!1l=^l|I?~uFfyp$*x*rDEIA%=!gbI$Fn6{sd&=OSY#moP~T@p#OLQ%91)Z2 z+bs@x&TgeCbn=c8hMN_W+&mm-Z_e6J#|WreUO{ypxgKvlEWd1TuFcTMvz*gqb2_aL zpIML87!1A?AODqFzPN2w2*zPY;Wm|S?vu~;0JLhg2dk=X>_K;9z_Q5w#~6sW)$%ts znhOp;KjHP&(X43eo0zz#nDi|DGQ+KW%9U>?eWcl=(qL3C4~Tc<^akQTwid}7+|W<1 zAhm>0^V}$94oWT-=?O)t3fE1$A~6uaHXH+EHnp820AuBffW4wAtdUV=<`5U~$6Lxp zDC=L&>We*a=o6yId(yeoI#%U)DvH26Fm>N=jWD=kJJ4y`Qb;DVMRs2{6Xi%nlJXs1 zL2b&39UaaPdEht5@uzi$ee8}1w>-@mlV2NKGgmgd8IbhR!@@v=({(R&rC9Ww0G!Y6 zylkBml}Pgh#%MJudSy~TP8`aHIL_!d@(kuLGU35QcgvGlW@TD4^g8f5NyE~Y{glEf&tOhDHPFz~_8Xd1&O)wsjyNGAPwsCbo7wh?s zi8f>)^t`qmo~2^iO7{Ihdvj^+0R_aR(9cGkaZXx2-$B3%j-XPS5Hcn7;^O1&%f$jC z_y;#MS8pnh6v)stqPBC%-!oQw3$bXou-5pDzY$A%07ZC_v~HN|>yGzkwP~BD@x#;I z4JG5&v~h*bV4dc$PfTu3)POML48j8~_6IUA-zz#RewHg)(iK)B8F)&o6(|fkgC~s0R?F_?5#F<2i7nRCBZfmUO zLACLP%jbO^=enn12XX~2(#6frJaS7`IaoC^oK9gg`5E`Dd*V4vlVe5XlF)gGr{K&S zaH4ZJ2a&kNO7AM$(vAP_OX;LJ|F@}#))VmZ%jQ67wvZ10wnlng+S3C&&qnO$5yHE- zXT_y`J9{a#P$H5@6LfxAx4Cs9ReQmG`2Gr$mN{?g$kDg#)IS+3QVyrEmusVD`*E0X zeIG+M{BGN}aq|RU9yH?R@>PY1Exj`~2$xXA4@LJNW^b@vN2S@u7!K8Adxu=UY$OcL zwfF!iGu%JP?rT%>cz&pyd%yiR;kO452V zd-LGqkLbNXYfa$(A{4HqgX;iP{sZr>A&om`(35vzNs`I+kf3l>rK;#AD1T}{gg!uO z*$&P6$J@VFRiA-4g->!tEW{r}d$}UVtI0;s3I?IjvJ(MF#vjl|x)bnv5`@rD7TU5G z5yOu6|EyOg`U}UVMx(;#*Xa}9b|c}TBA2vKG%go)$;Y5AgJ8i*wA z=Xzaic9KS*v0t42WeqN>%`%EJHy8>38a?HUSJsN$pYi?w(d_>Y&i}ou-_NdMQB)_C z!#6hOzhXf0%Ws1--NU;xsJw^`MIT}EuNr3l-3y@bm8aODHVcPgBQSiQb{}~1()d(e zE93RVe`9CR4-uU*x|fm4E3iXED%tOp$sR#W`FhTbeOq(@+(H3MoUHa#7}8xnADeob z-Vi=V8AK6}w%^-mY09Qq%cb)!V7Zoe=lo&*{n<$S)&&X4qZ41F!O}$0$2mm2DL91m z3hB#J_`9io{ZdSi?7jCE@3F%h_7aOg0jXc7o+PUudYAEg6R`%LPXw1*%<++e1THRm zq87aZS=nrP?ScGBwX3R5a5*iPIq~jBaAD(B{7k7<;~=w(dk77**9o}ztRD?P$xsK zRl6>dORnRo<*hZ6yq6kb-|jCT#TI>8hht3lqi-+2xNSV1*({nYrtuV4ZzU4la&7gc zQWC(w_~koYU21q7_=3pChXzd_mpaUmh?`43A0%QnppQNvD41CI4rO>sNpl+Cx4g-m zZWd9}Y%6Yu&~ij(y^oTOuck!rDd#r?KgUprHE$C?EIy9sJO2me3p=ml)5JhxqWs*J zt^DH`!AlGK?QFsVkik_}*!LHwQ~%vOK%ZsXto_9&dFBv|%Km70rhOnNjG85P0AH|P z90xu=NDK%0Y+$j!g@$6Ra{HNR<2AOo4xHs1d&`BblZm`L_>(-(^lI{Q1&4(}E|kyH zyq7vz1b2AM4z?5tPeouUzi*m5w@V4I9`pMrnEm=eIPg zNmbhXDg`>HvmLbfmQHc4{~Y0a_qN)1gXO~5Z~Q+f*~28yUFs6P^c2KslSRWL{kkk+ zhq_3eGF8DUv)3+zm*= zbEtwCOF|{5&VJ`esLq*IqIuoSJJV(63LlY?Cw*9ImP^16?X=PYm*iG4{$E&r?=Lt+ z6Cja*I*|8%PrM6V_^~l{p1C?HaU=f{(%$A)q?f6K5V~04a^iR8bEn#6=$$sFcVaPW zbdEjw`gNsZrKWq5Yca@rSfxlh?*^8WwZ?6G6%9NcsDk-V?j@;BIB#c?5Glevmuoy` zf7P#%b4s{DAa;FO>Z%w*xKPLcDQezw(;k^su-#It(!*o-#gfBrfx8Pc4*>xQ6AMcs zaU1sw_qMqO)B!)yAk$pYVhEGgr!L@<_FgX;YUa*xjo>WmlgB!#quSwt9wf(8EPIKG zMlPH=D3w&rPMR6aRsSq!_Uq?esN^>Jdg?I#u-;aNe3Py5SLYxG}`0%JLX)h&4(}N4=H7W0fAFW zCZp%?4_G~`W%3@)#zcnPD?|Oi{EnW~kxw6lrS#I=YsnwdG`0X;xOM}ZR<#*bP?nWY zd`+-kOE~uWq!WuW5af?UGOL&@dxhGPXI78$TW>&LH z-kNu|f(G)GW*k4cl`)AR$9}SYm*T#H`0{CyCzS=VKCEJ zZ|E+dJ=y2Nt`1*Mt*?l3;HntI5lIVJ5K)`cE&t*XOQQ|0Mu+-4dpxm#&=>oZPVUeHsGl0gfE~ z?UdY1o*z~)*4tikC*BU$Y-;|*%WZEe6boU)>mTs*V__4%;$EGB<%-kZp{o}hM2HsU z_5$`hE1?-nmmJ~T)_$&8r(f?Z-&Fv&q;6jea(R4k*GW5f!WbaBOR=5kffdeK73P>( zxV*os)N=_?{)VPcMli_r=BmiDAL~xLqg3ePRo)1%MD1IHYV2S{mlECBZ(u*&Xtk0O z>qqK3vx#JWiYVjW>Mt^~ue?nO0y2ks846mx*Pg*Y(@1EgN_?g&3LS_V?By2sQyKQ1 zaZ#7D@iX{J8Me%ccb`(NhfU>JeZyCywv?iI<{j~hjEYH(!sYf<4Vp+*?~EMZyUq5L zn^B!Yr|8ML%eg8U5jTxlBwu*Ric<~O3pg_m($D51l(g|7F#X|Q3)T--x6IGR+D*Gi z!1o@nF{>7vZA`ObfT^ks$>}?kx=nPd_C0UO4pnpe$N*!+tAxcqBI(HhNZQUJ+uH4P z^*e=7=`>_rK=P^weWw-=Zu_i`8mmihI8JVT0gsd7p!H6x zOn0I6#F6u4nY;A1;0JD5ti#Ot4d)5Jb9CBV7o&3~rSNBAf;sM_=?b<0nvgc27rV!Y zyp#JH^5@J)G61qNTJ;A?+3+ohF%0d^p+XGlJ#Bq~vv`^fA<~jV$9rEX4D&BLCd;=m zR9?6{elBHNZDg8XR>@YP#7OU)w z;kbwx1@bwak;1@3<5W8TCs`)Mini9LR}u?W z+7ucyNHQg#eD-r8xR;wXHsZgS>)DQ%=WA+H0L38HrJZZt=O0r>UbtZyppA>}PEZEH z-GXVJ!PR;K7}jPAx%tPmhf*ef2s{zG+lyH zfz*D$;DHB7`6PTsqcTzR4Ex4NgYyoe#oRD_I5X~7O&wttob(AEN8i@@y@l{SV?z`~ zQVD-(q5Rq&eBroHl!mi8$hhtEMay+aP~!@nG?X{m5;g@T9Z$;JZ0X+_k43vM`Jt1S zf!+!8b9kfB!l0ot@b9rVGXw{Hr@(ptZXRJmf|a%fQV+diP!vS64*u%C^*y{@b$tWH zf>Y{70@F$Ad7j4NNMs0jOPd_G!j6tQD9}Bi#I{Js-kp1CZlWYdDYA&8j)$dU%c`#H zsRajm-cpi3!j2JR?CgJ-0o7A2vCD}1g!J>!gHngarn-znFZ~*D>?RH&>-)8cVuj@T z09~b*fStr!k5*+0G_I8*N8a1Ws6OYI&~MM$`o?!4_gzZ#;Hidy6?E4-K=; zo+G`Bc`hHTs|`rTgW2zW9x_i0)r1ZQbQKY*MY<-XtneNpxXNIP?slaZ)k{4Fg| z>R@MO+1YU z>|{)efoJUROjO^jSL6%Aa>a7SJe+Vz@*=w-x07BS>8J8G+fxBu-nQEiTC3{L#<4u{ z+*Z^x4rkK_w<%ND-_ZRVOgRy^Oh9_<1B@wJw&=S^XJf>}Vken+lDPr7U6h|Ii zgR~NA#C_JsxyPz}+e;0g*F4q)sZ0-+Zp@~<5I0+~+B)0PjcW=r!tOk(^HF})^)+!x zBYsBw$qv1ykKHrf8t_q{+=?ax4hY*f**RoRnJB9y`yLu?WG6eM$;=-)_XDNj(0aIH z6xl?i)5S9jZsV2|m{(J8-LLQ%Z%8CctdW(U^W|b=sl3zZ3f?{Yoz9}iJK>gYTESod zpwEg7nnJlr^v>XTz6Ca)0VNR1r5EDAr4Kd(q!QcCT3O=Ip6T&1@aT zNd9%cI>vpWY?+gjiQWcDyX~mM8D=HfE52g_8$IZ}r>EIP&LH;!y_$E5>lOayjR;53 z;arL1J~T{bCd-^Ty1CYO2<~PT2F>T_+QewjN3s>&|0hrBq0qv+ZwtT?E_nqrZ1ik!PHwZme_`9r-TGMo4tB$%_QQ}mIwcMr&uRPTcwluv| zYheO-_Z~zBcVctrnQvw*v|PbS#|PTwNVmLB=*lq6DWH|i2S5_3)YaF0J9#RrYoC3- zar{{ENrD9`Fxvxp(!Xi3k=WK+N2X|U8phNT*}XcP+bOFtB8~*|5+R8Kal`~gd6y)v zC-&MR>bKa~=}`Rioa%;u?Uy>3X(u9xGI6$+tSN8MS#>w%9 z*FBpGJL=gb>xANyFme{%@`5r4aW#JDHbdPPI4oJ~Qze3zhIY?mBm&zkQiHP$rHeRdreyPiXm`7-9KMKgJmPod4jONoMfK$ zQq|46NUY=0444Co16xUuu1_WrCVQTIH=aAW;V5U_Ck=E@z{$^mW|~pcBxSkJ-9=TX zd0PlLRxR>=r4t;Qn;?1WMhP4A>~3`$z)aSb1XowF_yw6 zwohlj9T7`$g90&cG?0e{N2e&T(j}R}bPoTDl>hKod zm*dfoiz_M8*;8>yPk!8BV_05#0;3o>di#$MED_X^v4tri?A7H7!lW6ESsMByas0xs z7MN>F`6AaXxej={FBFY$V|IR3l;f_^72*m~DA_y))&9qBL$>_HZtKnEkmj@}z>s=( z{TG*yby09&I=l<$Of(>1AbNT_2*6wSb_IeB*d@{e5E`vNY6uI##?R>AeZALvzI*5v z@^;4;;3@skl8+0|KC8s#JfX>~IOu@zVKhNgtj|~O-0$>npa8RhHCr`Z84RIHpT?ZWQz66asy2X(R zfS%USn8kYe>c6c0nu)n8&Bs5|Xj`>t_ zAwtFVVv-)ajd9z$)i`>YQ;+Z}`Pzd+HQsrt@oEsWzn4mTaYNi8!Wo)gZpwo*Y61z9lV3&t}#k@6zlCD1n9Z)b1H&Eh#p6w2&u_!6_M zeQb;{Uu53c?c-NQ!h20*7>~AXY7!Y2x7P>ySaG+7?uB+2RSB?~MvH$PCOkv9NDerl zbA^0Oqfs}W-ch<-cHgLRnsr*}R0Hy}q~vmy)B%{H#Xi&z-}$J8PB0|NBhi&~j`b%i z8mMxoSgq8qf|oAt>jkSAWm{6D_;q*~aM^z?`kHn6F;(v%tMA7f5wrbsmmX^4gZPfK>*t9^bu^`)+|_c2 z^F_kVDUCk(hzsL6SmW|9$)XxVn=okk0%%9LTNS0SFJZBz2R?{P@d`V8@y+I%ONC^4 zQzfFO6u(YF+jP?X0oqZECFY(7eP&;~GLZ8FQ^EafEtdg3nvFsWYS%*sl0nIRM@N=refLp0m*gi{L&s7JfMAXvxb{xW}v!H9RezjoA0 z;)ubAq+Tf>Sx&A!F^Kgmw**nqY%(NyiNulF-}f!p55d(JDyAD9)w^k3@k(UAF^f&&9+P43%hmH< zzaa2TE*+Dcv3)S(KDd%=#VOpXufEJz?61m9j|~eE zWZr7s`eeI&*S&k+B>=)=@Jun?{I+tpYco12ZvplGSbC;_JQi;5;Pb+dmE-(b8!r27 zGvQ)6_#|z!FT|55!7d1jwt-roF|7qTK!~*aK(3a)2HNdOgx3=56d8F*Tz;(kM! zb3$UF%Nw7&M7`*%79&yU96*pHCku*# zp5`ZB8&sClnXsGVH6iYh?d|!kicYUiHp*Ml!THpNv?qsIK|4bde6? zb2wDv>OHm*M4)@GX^wVz+5Px6D7{^ZGy*GzNT_oKM-#b_3$r)*c!GJmL}kN3@6MMr zETB(RknR_jsm&KxSM9KXL`wf8vKG1PWq zM#n~_P$0K(fh0VYR9!^wmMM|_`0l(ez-7#2FTxks80mS_K3HLnbk?o(_?gDJike-4 zQ$Oi#9?R!;j-N4-3j#9e4c{>VvS(SG~*Xo|<> zz7MbfLk@Pf&e;#vyr)_>_IyMS9$ODcn6}at&lUr@YGrVuy$uB7{G&^Ds8L4Lf=jXw zVxT_QexW;3ijG^w!lNBVbMCfEk%y44lvU2OhclJbvNus3{I>K8kX1_?tuyO|s3GO_ zy-&3kMOGyb7@E^&g#cahbAtjs2_Q0aQuNEI3A8vE0r=ji8}MxCXFPnrDuYYon*)#P zS1H9_@1J?7TSsU^fzR)Gs)0qLn#n4ky%d&#`c`!!DRX#rKC;u5AYiZ^QGXS&@ZbaW zzhP9;>5TjHSUXYm6|3AFNiN0qwD|ht4YOMhi$7PFR%60)u&zpbDcky~o!QxWfy7B# z$Nio-kKcMw5Nm+0V;FPZaLiWj7v;*=o^_sEpncgPn?NFjFr^)YjX@dS}duh0-YK>J#j>#0;$jD$*n4Mu)K^sRfN9YY&}VVZA!ec zN9h2qy2~r6eD)uQM(5L$ko-3QxJ2ibZxmE#6oP^=*-m=V#MVo$!)u z3DICghjV-Rbi~L)eJ-IPRA>BJFOxSqnts-+9^{ z5*R%4BaHmfp;y76Uo`}QB4jPzW|Z!uSwg&DIkPp;zB^7pS5z2OPrT;hWS)%&n`Lx$ z*5YqX|2sM!*>55rRPr-=q<|hw9gQTU6#uED26n+lA+!D3SLdEj-X*+ptMv!G7LG~J zkyJIrZ+}O}?%a@Sdt}Y%Vu@W5?5;%3 zpPhg{u0>?z_^!#c!BomAmo1rL>L}@-`a9WV3ZAZ$+*QDccSonrc$^yG0aW$WKMvz# zJ{ALIypZ75Vzsld(Ec(po=~*QcChByH!C}tH;LaV5W*z}o^8)rCwn+@8ItB~cm7Mf zonp%tMhG1-6P}G8MOr%`5c*sy*DC9Vb^HolX(ReH6N(F|&>q=zg5AkY9%ke%JV@wr zVLJfbiT}F@lK7aEm-w&3{Zns;%sU4D8=3r5ODI$RwH-ua0Ga0haBM#3^Z)5_Yfn$= zzk31x2e1Br<*|dSUP2BKX^PznfVW`BAHxx6O|F24`o6*-;#t33wMmP0c>X^gT)h27 z0y=32^3R8tVc0b@W7XeIYtv4y_t~$;V-ewjS{@R&KwGl8viG2kN`a#22mUHWj%W@B zdyDordEe*d49~kY$x7tP>$F(#hSP0gkCK&S}5R z{Q{Ld`RtHm>VKjlgz#P}t80d{!uQr;eMQRD<}qE zPb-yulCT^9m@i%XX{EMB#}yR&FHk}mHYM7w@7VI0zkE>F?C}eX^yPiaUd2tyDOIHL zT3C8RlWo@C$u&wla3nd#%MZWZ_P2^htW_b=Cz@D}53qS!jB?w=mDi|Lx$|1!FDtbF zdQo;fn~f>Ax^vsLk<2OYxtRK?%;(ip6B_0qvX-ouslV+X@^9-S&@mGV*LrO{J!T^k ze0ZY+aGtQdeoOqkH=7gB+N%w{F14T9S?i>7p9N_7lW`u^ExH7Pr!E?eFME7i_^Z-G z`dH7QF|P7Du1N*FdS{c^J=m2$FJ+zm>u9xi!;+)%XN&^b>i0zzV_g`*5xLf-+`E*% zU0zXk{K;#uJ%BEd^OyG8*K#HEg%U?ZJ5Z`g*E9MWm6KEhA0VwN={2dj~EL$dw%Io@Na5bCPsFLlO$*?JKXY4);R zdO= zC3Ta+Vn`z~|FTGv@Yb92ll5dkwISmyp#~eHh(Q|bb7Uh}hdED`>8ShW?5-TfhNtg@ z{^Ni9-{K1+KKiqtMbF}Zmm{@vof0T4KHbALd@%@RN+gs0v4)&zsr@+Yiqi0PG(y8q zIk`?f>a2l73oCm+<*GzLn$Y{T-~xERe0Fr=WW$xUr5)HF)kYPG!<=U}Mf$;Qc+XvH z;6`1I+$VwP<#@c1$Z>IL`3B@7z={#q#`kHVEn>&|JL0@w=@pGSn}Y$1&?P=wI&U)TiJM=8=TVR6|H4gq+bPY%vN|dHymwcn zpF=H+#;fGB{RGF)d}vrKzdE*Ud%%airI@!J>d|JckjDhcbx`3j6BS;q=rh$4rCp3| z<49<2i$7R-5#Pu3%~Ts78y!ToYI#owxpguHO$gf{*Cp4>zZ&X#COGKU9v+x4;WNK2 zmd%!w_YomqX2E187RkgUB)3#~F7RmqzxFo_%PAVWxXEs`+O+Ns$uT}HY?NR2h(mQ# zLP=p6B(9$S(FN(uc2G#+#`c6VGe1GucpA=%`~an#bpx5bZ-gBql)9dRA#QrRLBL$0 zL6q4_DV97ITvgRDRPUx={ww(v&|BJ5*!x5NQe@vT4-;6MbR|yHBgB0hk#TKr#3;@3 zo@^+i^87Fy0RVCrjF}HHf&OCWMA$8Iwq4ZKB%s#yk4&)~P+yqToswdTzi_X4)jb?_ zwmp9B*;XnT+3~P0SBx<+|88G#b)sXkgdfX>POb}1Xt5fTE%kbLWYO?);D?QT=3>Y& z7aV~iv%$!YbH|;7fpeC(fp^UqWvmd2rg?9n&Mxqribe92ZX_wS3L@M;0ype&JxG4X z;ZN1nVVEL}`a)eHt9`WRxHIq#PmR3MbbGps=3gHBQpQ1J^Vq}M5A*gu zSL2`TOp4UvtC){*`a;ev%jWBa%%7h#iA3>DW)HVLv|+j}z%Px2i<)fTpW4G^cAcUG zDO1yf3&XZ9h6t}txH8Dt`2RQd-YP1tpz9Wm0UMO4Fq=@cW4@GoFqta zcTIu?cekL8G!Wd~wQ*>m>BIMr@!5Ge=jDz&?!(MpG{gUvO4~eRz21^uKV6pnK(DfOUjInQt7yW zht;i=hoIJ#7_-uJe4~{(V0h6(a+`-7|f2{n@Lr+yiWp=#{}yIgt2Hr@x0}1-5Qy z!_Qz+iE=DSzxnv5TSxb=dH9JJ44Y_1o(T*Sn)3=B?;#G<-F`!QVPS%2ML)f~-I?CGbd+a@|m$Z9J&Tc972a zhV~m%g?M*}7ZsWMDmEP3U?|<*7Wr3{Jx!#taRHN-r0qMzYAW*TV$?4LR}N(2_dEC= zN9pt0wrM+3y!mK>>l~RrVNt9X|0vjkYplo6Yu38}ji8+S^?1`3sLs@V|fVw5z*%W9r@4GOkI^7c(^sS8CF%jg5u|heAS^GWNzn z5=PsPbkXfg$cJy6;M=O$y6JO3bo+#MbC}q$9GCJ|?yjU!#Ej|XCMzL;htZ;QL6Rit zV@!OfS?AD7jADL`3I9)agb5eaHPLjr(oXaIiQRUOhXXNt?U>zHMbYV1t=oK_T z=~Y@N%+0%wQ+2!H8_TD*j~yom`DU#XJKW(---^_}#M98Mb-aBZ{A<~K^n#TByXKE# z3@NmHzTYz>tc3DPUv_JgFt2|gR<*G?w$Mq>#H+pw1C7^z zQ| z4#1#M!5j-XnRJ`4jIEB-^IUCYckpfgfcMQ4C0w7e&EQsxR*&7U>{x|{NHU=<8FX0` z83N9fe!1NBLG(*jE}OrZZW^cWcmpq187HN|366O~uX4HU$xtRgV8Q13gZUC|cf2kC z?y`{R3g^YIJB_Eaf++bJ@LiM57cFMYj9Ydt2&+OTutCNz=;#^Hulg%!bn$jF>#kLp z_ZN^?>u~R@%(LxRAkQjyBxkrttC{=B|?6 zU@F>>LhXnOZ-Wgw)tC$r0b<1qz8Id4TmDOO=Im{a)vr%Ku@@KpGmT7xbNw|l-%&tS zjCl_lsb&LGdu9@;`6Hz8)9+Y8E1uEt2rN(Ht3uri_Wa%{ zG(qeup&YpW-yHR-JG;)|;a47kR9M0}p7DBug71%q&?Zy&mCi@py=ola&P|8_Jqw$> z)!MeR&ES!nTYO;0-^ED7&2x%7aTW4gC9JG5VJbWvGT~?N?`NgUa0(Q z-!sOTrTMoWEx~r3YqQ{vgocmJ;$hPtn7u99`mceg&Q=^|VdrfoK}Qd|qq)#ZvPz4* zQvx(k$fCb?Hk(QjKZa0z3F(Gni9+^LC$c9;l*N-zyX^B4nhyt!sR`_R-24h_Y~3j>9}7xooVf8 zX-!qgSHxXN@L7J|3xl53czplX`%MS&I?}V?Qx|!jHv=`v{M9!iCKQp5&w2tkl184neEDnkAQz;JT1IU!5z-PK^CYcRpbtaEq#?conlg5V`g{y@hR+%Y!hK8X5 zI^0<0Rx{$|&D9%gPJ`cvIhZvfp>2YS8lwZnaT$EiFNyFPmeHYVcid%vr2>HONPQ=$ZqLhX@Cm8O&a-!d6=jLvT~<`>=&y5TJFi$ViA5;?Zz zw1?0*N&oOuSiWj8A2D1DirPF!zyAFN{*kUX;g!*M^-l1qxXHk7bVKjR3Wdj+a&qPk ziBF_#)}V&lA8lj*CD~D?rU7rHSjRDU?H(|N`JPhp!#{g%vvW&7?-fnEbGo&p0U#n^ za2y%dN;u%q=zQ^FDCNj1!I~%;P^7XaFy&=-sfz7Ak=1Dwp~lmVQ;%v4!>#ukvjr%) zw2-#){+U9lRBsqQ(Ww=2{#Aub548?JC-?6!YEeSsT;D z#hE^qJuaFV&;YkQj`IbO8p2K|@K4?Wzb^NZyf@vnD_v65m|96HfkvNhtSny~>P4>O zD1J6O73doo9t|4#Y9VrtYu+I-sO%!+_r=HM(zr5-Qe}g5BagK*1z6tqbhnM zEv8zwdUOQSLp!<+r?2u`v~rD;?}{&S8T2qhuu?vPhu7q`y3$TxOG054sO`uw-m)B8 znxlQI&AI#xVQ_a03C(p>y5Z>klVln7Qe#cuR(8`WJ%K0PTOGJnXK?v&cZb2sSEjH4 za|w-Vek(+5i0|SW@&pLYIFTFDu*r@xukEJ2^`9lm{I*kA7}F9f z-xQ$hN}LP&9^h%dmxF!xH^Ku%^wViL9Y(g(IcwXJ%414nN>=mvr!k`oL-sP!KW#&= zFHP#LMiknF%R?yw`=-{L&a!pi^X-3*Lo=`EwK0{Lz8+GJ@lTo)HKiIBEvJ`@`*`8mM4T)*EqW{^ zQXR_zRB4@Y*h=cvX*9bmSlqxNLwqW@gKH~hf4!j4$GX$;Nj*Ew$;5J{2NjvMO4h&T z=Y2w^$OHbxmLRb**eKZG+UXg8UG>fF5MN$^Efah%CRg5mL3vK7XXRwJ{XN3-x;WUu z%jMi6gWTeUA(r-b4c;2|-95%WSLFslYKlr!gqq28((mh*8%B));xqo=fV;X9#7Us| zWVU`(xVXrB(K}ftcKv~w!Y=U8u~Hp+k|*5*qug~2!@-}Z6XU4ahcT;p$vPa>oI^f4 zwQu1q(LIPKI;?`6ve{b(n5+g_+fTVM@f$EI*yGpNrE92T9`zGGiegx0S>^hGC62Qb z2qH*$JrxR6NrwUbpm@Jrq{ZQ~s-xhC-(QJnqi2zJGI{r-LesOLNVBt$wQ z`P9LaDx|)mKh&GkP#l~&2QS^pb*G(8*l((LWlwEO(f3pO8}OMrn*AY}sO1|K&>gB9 z$*od8rtl8fhAVV+U>_a9_&O=>pSVa~NvQP20XUwMm;Ce8qcoa{*$U2!NPFe)sXng3 z)Oqldls&;Fk=|0NO$Ax;j)>xkjET(;nXtAP+XpUWemy|r?iNGA+qc2@Qo`BLO{Y=I z4wif!QRUTRaSLiSmC zb&pN1mX9p_P92qEVazH6Q$h-Q*`Nk#4gu^@fJo@2=D}Z`l2F zJmPuD+dN`?izAu)-P#xp8pL6b@aL_gk>Bb2vkT|^fQH(Xvfk9my4ru^tbqMx#7VX8 zuj(IuIbK4^ZXyBrKxoOBh5KoUWztuaMB=*U(hq<>eK}s6Zx_&i$5va*t|PFK7K6!pSH>4FSTOPKak&lA7YCCHPZ3G zAH)8$Ts-FgcTXqEr5&-CNRkg=fjBHdp&Nr3QWgw&Oxw7kcx&~I_6ZhU(v8&HjeNp! zhN3>%tBD}lV}62>%h;FD{L@~0IosHH$Gr5l+jcnV?b*9Nl*{Xujs3f9=>UKh*+P>S zk|{1aBEb~5?c?9x&+4sYO#ykuetCbX_EHoucTTFqS|;VcQo8_R+6JAOrZ z=j|6B6=s*;+d(T3LW06*v-LaG{k~L;!7L$oJH>GgDHD=_1y7Vr|0myE`tWvWH{MQP z{L`CT?bS5KpQk!`dM|WTvgDwdwPA~*IX~fCR?CxPkw`@I#=a=jBed#raQ(=DXHBw$ z*~{GCMdP=?Yyau2*5ErH_^+>jFE7|MqF$jElodnDLOI|XOaaetD{wFt@Y;7j$2=P@ ztPva-o}az*$2d!->5Cu21qIeDHA?->PgUnLG!je>m&y1G*~)LBzF4l$u;0_FeKpm0 z>^{y(Ij^SA&=OVKJQ5Cu^LTaAEmKv+iYwF)Ul3g(-@0td=YejM8GH)i^7X&hFQJ;r zRY>+45iRD*OT*U->SGU1_kf09b+#9$51Py-)RfU1MlVGBAPm4Xf0%Ic`pE+|Z;-Lr z5F&#+ps?C6Z<6XDMd9U2A2hI#`}%;3>}Eiqi)vSLW3SW77N9OhI0x+!C1HD`1A3R6*ehP)_ZgwU<7uef52^(Z+!_Yi!Z# zqm0!h13Zw?-#1O*bp84waeia@x#EtW2;9|8XY}q^rZl^t{fM-rFDaRa$8d@JJE!}> zD#_=sT@_A^5!~zIzfyI%tLS@sAX0-1x{0E0%YG?~HKcu*kKrKPXlH-lCeExGf$fDQ z(8S**a2o@(&RJY!O0PR}h{zVmnETlCBk487&w=(*CW~&_`~>+}3^^-UIPI`0=Z@c! zhqQ*Rp1i#sgzrrS8%-l5JarfQcN2(!F_Dc1>m4I;!&a)Me=ba_k-UMjmrQFSW@wUa zu@r2?rPRh2L)l5NY~0nKT|sy*-N_bN{}@Jb-yHLzNT%lb^6wwAEa0ENcR|{2j98Gc zx88}@p$(Ia5U8NLkTi6O26<1Sb8i2t0V2)Tg$M~&Xp3yj$Bz5G zu8mwO%t_@~D4!myu9@QDNq%cW!wI+-sD71ySlc9Z4GZ zYd4Y0@x+4LUxl8uT(kAG497?Hp)!WDQNrboN~P_Z_Sb~s=ZuhWPiApX$Fq^Jx9p3w zAGKP+qfI1mSnG<&qwyrl5qG10{50!&Pl!^74yg`?oK@Ks5B2+J4e2;EN?Zx~HP_RP zwqZ{k6ZiJKV~SSdKFvA}t*i2(>=tTHkt6xjw|6<=`7qG8IbuYOs#ZNcyz2mq-QG&x zAtXIfO;>~iI@y^r6dJi3HU&EUKH}eok6F?B3_50_zEHE0w*lAIk3-pxrdL>*wrUhK=}^YHhReU5v^58NzED*VN{7VZ_$ zHU^-ai{J@njV*B?F*drD3q1TM7vK!{j<};cfv99HqmcU282PokYKBeL*paRx5Cmz3Pabvm=cp2pofG7ydOO{q1uSGXG`;9GFR%7GoY;KrFfzn8 z!ej_)>F*nmw*B1BI_IiW=oS$Nn|0;bCwawUlaXptKTN4G!OA~?_c`oaoj0%Zh%slz zuED1pt)B;zTdm{2UkfWmZypR7oet|oWV3;m-k3Uyls})X(Qsiqd@>?j}!|YY=m6Zze>xvx;@_G{ls#2>xmys{eI1#=mi@;p0V^QYn_CyLOB z$zkK3>b5EU=-!D~#h|CSd(WM%=faeey}(_Hj5Q{ziEqW(!$Y-t$8KPuh2SD6Z;rEi zEn%j}px~9iOirF!2jZ~RqeRMl-dpnyYwJ%J#74ady}hYKJx`ycImV#3Gd76mjlY}l z5^ExJz(U#~V3Rss@O9Z{lnMt+Y@wJxCRgGrB*ueQA|`8?Y;#_Ke7D{roxmu67d4%ZK;C0oEQ;V|hbhfw|j{@x5z~zn)0q z>5`XU7sVu(0oqFIAGMjl2c}X;*KR{7b`lMaUSJ<5YuAJ%J zU!QmTH`$EWZ0ym(Co&EjbuRg*N6p_RjY+fI^h9e1LN$C*R}pGQj}qpJg8QSD6)3tj zBlW{fJ^u7TuVVG_}4~M z)Z#UZmh0n)fPSxvgVnDqv=8@CGK)rMNLPfopAKn63>p>I?5#NZ-uN24+A5PeX7%$7 zP;$QD;!O~ecv2+XzLI=0aW4R(b`{KOifABL2u_?tNaF*^V5Xui|`N-4AO*Rk)ZnG#2lcZ1T$}Z7!hiPEaarQc}KduA~1dnF$9nuZoVGjmYOa(F4(clK( z@iVD(o{AF@5}>-KW?fg}BK^ul^RcwvLe0xO_m_ae#1HR#dIlQOE@Hx{gs5mJ=XH5@ zm>U@Uma{Y+lAXoKd%4zP9_9O>_$*8ptRJANVD^y?0uoRkmaga2m6=-hq;*a5d#e&f{(k1-6S__EALc8-J_GfH3peJQV`Do{EGwjnL3s6`dG6qJ7Vyf4?amx1(MFLDYrkW;W-LBX2en6l+ti~znA)4 z^}6+Zx#XhMGjOUVzzXDi5uNdQdUQofY6VwUpbZPhpdl~W`h7{V<2L!1CYb8V{oq#V z<=!$K2JsWuDCL9E&yK4$R=$f_XC&HZ`cPV!xam*LiWohFp3ke6X7%Q4Xz8sflX-SO z{$;yv+#%}FQojwsJ5%DZfjkZL6uCB)2&lr~;tV^Naw8b5U*S62Y{{uQOsL!M1C5WH3ntuk zjE*azJ9mnnpt*-KW(`akbru6|sx zfVY@5ZJL-35wwk7J&ySyp&M*_6aIu9+kR=F9k;^PZW;dVd%N2@ib2>!zNh=RC(|Wt z^s6c`e`(gJ;agcgdcu@`8;;Wh`3DEabVhsiuGWo(FofS`bqDaf#v>vV?tdPTf(r^R@`gKIMZW(^3D zaTFZzCqt0XT#}7)>51~%54deVR6Nh8uXvL_ZoreL+2iqW4SQ=$B^DdK@z8pAJ;{$} z*hb2mZM?d;^*4w>yn(H$IvVP1I-u(v;2%A2E@8cO4eJzE^uiWg9 z5`I$W4m!F`CPo)MhA$R6^g36LllGRYo=g)uvLylDEi@SABYYANc8{X#4;S2y^ZuQe z9;l6$rsuy2>c2wn0y?K(#!E_9PlJ(o&MCDVLoBD+%VrK9l5W{!5)paI!zqel)QcZ z+~8`9Eyp^oYL^~8QD`)N4Rnr>6o@ZrXqk9$Urvb~xud+>q7sKkBeiZs_5CzyoA#v9fpRbE@8a&94DXhaf!yVu%-? zK`f%wOK)Rh)z$SqkDXf2vZ}IHNA>n83p1cKCfwtwMF$X_c{$A15zKt%pCNo|DVQ)- z5$xpyxxg^ivl4lc$kV$rYt?n61H0!;0wljOhlON}(U5vg0z9iTPaRQF!j3V<9yC=s zdbZ6;?2bHJM<#v8;m(FxOe}WvaRC|J?n6Sg5$XjOp;=6eq~>(E8P^nv+eZ2EC5ngw4K!Id7cRPHHk!L;x4>dFxVYEKz~06pM!0Z1nGIXmuLBMmE15GCj@I zz?`)=wbGqlxid3q*p;n|av$N1@5pJDp_<{}-ICVYl#x!7jF$FEiNh^$&!9DNV8K4A ziP8vu<*KCo{oYc!3>U>8hdG}ty@0)Bahvqc@_wGUupM2)7#4LO>hVtrTEOI9kpK=@u!?k*dh8bBiAoA=Az4$2YessU z;QPg<1)4nhWmxm1Vw$Wz_bS!O{A> z)wv=8@3|`R~~>yrnyMB~hm_!g!R0*D|0Qn}%yZO!1cT zaxyU0QHOwyF9QLuu?9>V9u50on9YAqof9ceJ7u|O*7FcY2vAn}{3_bjnVuv^HH0~i z*UJ-_@>bpDHdKOevdFi)X>>0~5ArwsO&cm0-GQ%RdL9;#F@9#uI(}DBzdjyP(pTmDl&@bIs{+ zACuDR`F2EtfVq8hA?NiH@0;87v=X@vHiU-vFBj?_b^Ews! z`eGZMHlGgZK`#tQCc3HmR$p8anh`@POn8VWY^P=6`n&sUOrU7Rf;(K(foX$TBRkY1 zXi2%@whR#*y^D{WN?_~GTi?xFZ=H;83F|996$YuQ9T4@V%Q_)huLz7q|bc>9F1oAW9Wn%n^Yie)cCkFlmZ>nQ7v{i;u8rD9`sLxDVQw*6|(?nCG zj87_1onkx~rMNjxVM`Rh6Q`hi#e0(W%Jbu|hDv8P-Ef}AK#8OWI)zNU-$@px#N&gs zAL+&Z2Qzq1Xum(R(qBz63p)iqJ6(Rds@NClqzmZ!{(DX(Ydw)8)0|!(|HcERE*huJ z#;mgVg$E48G3gv)dro{fd-!DS{(K1o+Y^Llpc)d-4N-g5wD^Zq^s2GrTXm1{ORtol z-U@>cglos50}AI0Cyvo!n#py&tY5t9h9Q8{J%_uL*}22LhXs5vJ~7_&b`eU?xs zWyII%%NaUiL~ZA9R8Nfb`yO0#&Tk z*{6TAu&Mvx_-HmUOa%N+X0bA*V{htQ$N}M(ithKwS5dRr~tc1T}$9r zV1pm(<15^i?4pn~Y{_s7%>qf?#?hp7bA2_KUYgar54~pPzIfvG42O=hsS zn;24S9*l>?^A?<4LZCK48{wf5#6-J+qc6M1gW_eq`oQ|vLQ(KDi<1jCjKf*S0U8>^ zVkWTfX#8xIm?O`WPeR!paMLQ}@P;8kS6NUyhy6y^F;mnBk3!g~j(&(4>B0S47*_&C1pPpA@y*i5WLc`+*gNKJPTirjL4kH-3dMnkqoU)dakiv18c-rt zRUP)!S)i}JnD|NMthy$MfoLVq{BFi8hd%3?k+E&`?iT}f39~^6S&2wYZbJBZIMknz zg#zLV^^;C#pfM=4kmOlzTIgHE2@j(Z>){5QJ=ZrT6&UD3Xkle-|)>_|*ML}@*J zLgo-jK}G6hRFFu1MF1ncrH(k{&~0_af1xzk<~i9en`W*V;DXE$y-4Ry5zytx2Q zcJM+`n~QxA$VQKEN@P@eJUc(7Ji5muA2u}|2-9cZ;sLkOIc$!7vg(xD&E^GLsx)m!d3&?u zaPUyaraM|}US~@pRY9K6drx+~XDqS9BXLFnVD4>xJh>a!6`WG9^Y~Fj0sdAz7Y)Kl z{5Q893v=$!yWNh*&*T#0Ty&9KDwCTez~ZI^OV}U)Tl4{$6(||A))b%JfF9DbI6GSZ z9ItVEMA_*(QrRiM@Y%DMm4=(6RLsA){MsfA2M!OULqt6iRl<`f8B64@hxPf2f$?8b z19N-BiMoel|JG9ZyY+w_8jx?hdL^o;!#JCX+uSds#lhw$*Wh z?^S+e^my*X3vziq-d@N1?rL+&-%7G&ME7JgCnFKS@+fU_JIc|w$IV3h*ti($s&H$EGSk85YW$UNLNFzFU&f-caE9Vo*2@U0HP>E0wahkIj@KbLrMaC zyJzkWEh>zY)LDd{dU>u7ZT6YXXhPwhnm-J7=z<(|yMhLkW=&Yf+canm<-2ce+&k^M z8o|7A=IZ1O1|Uco@}PAzZsaZ#S7Xm_nS}9FR~*3uV2xI>%}hzJde2{FXP0j!No{g% z3&=EvLLNP1Zm{7tt0;uBH+d|_7tH%nMO5qdI_x4cY@%eiM_zy_nqGNBT5&`bKgrM` z`G>`?f4IZBJ`E(vFg~9m`rdiida#Q2t)$ko6eL@!&<-V{1r;OEZhCm{`L_Fk4a&vxoX?ym>8K?yp*aqamQU_$^5NlilJe{(Ou^%N9FdryC z(4+6TStvW1tn>*3IG3ZUtGKGodVw$ECcP(q3vU)`onx0~@gcr# zrJeiKgW3H8>Z29y+Ym|~!mJvC=Vr2_sWF|?5mh)mt;em&H)hm3&cRW;CJArV?P#DNSlA}B)ofNwDvh@B| zGfI@PB@CUu#+asJ@`w>;FEwfNdnfJ7I>jd2x@3@Ax%ka~gI3em@s}O1;B<7M{V(st zi<6CpEi7HPp`;M=kMqRXVCQJ_x4WAv1r^_FS;UHr-unhp`prdedMx*o85K>`zMx+y zx9pRAO-N-B$X@^E9PSOVWCFNdN@w1I)H*tzf8EiZs79J`=7WaU=iCPfjl{0Xg|-WQ zR(?4g)nzknJzk`*f-fdIG<_)v=5~78ex$v5G=DStuAQAq*?_Xpq zvkLzl{>!`4ilOT{xDQL0u6VlZ?i(3Oo;hf@7a%8{{Na_&=)j*Wz?vA!O7 zxxWtJ{uY9kqo;8F^@?wVga)3Y%>y37;&HS4sK@;FMk6|G#V8GvzzA?ZpGU4%kLyS; zKPk5)i79z?S%3m%ZkF}gECR`NEZcW>3rt{CYBi z?mUv3Wueo+)2K{Q%$L#czcUZZSYUUvLlb?~aP;vga&%U}?0wq~67QDhXM@GXkGa{0 z$+{KQh4G|!v!2w2p?4KLN>wfpvx#)-L~cq;^%dHSH1AmE@*;(^TZ23a^3F$VXsVOV zP`D}U5-RbHY6^oH3ir4D+2d4EOXW7ioU912=Q^np z^tTIzmk^K_p+okDd41?P~{ zVg0d-g^mGThI5n}WImx~20Qt>KM=7h)_k2gI1inBm$X$EaYVF`w>jvry&7pcSu&S= zg$36zIU(%R_7vZ;ipuN?-%9MiHf7Pe0=L$@evA1CZK%D8E^C$o9^PD>86X)vLra^Vq5X;E@A1d_J@_9WFaLmIQnJ7W@HC+UENq1GOpy z1HyhOzgqmiG+G+`?I>l_(TORTh>glTtC9#6B*Q1PUJp@l8$yIenahrWwn4Kg@Zml! z$5Xerw+G|v^7<8oV)NVy^*H+Kg>cSiY&cdj9fo)>a!?=t3fGrPbmk91^_q?rzXw!V z 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 j2;-lv-`D@&RrEs0?MSHb%CVXKtwB;$R-{TuFW~ "inventory_fde"; } +# Duplicated from the CFEngine standard library so this module can be parsed +# and tested standalone without loading the full masterfiles. +# _tidy: lib/files.cf body delete tidy +# _in_shell: lib/commands.cf body contain in_shell + +body delete _tidy +{ + dirlinks => "delete"; + rmdirs => "true"; +} + +body contain _in_shell +{ + useshell => "useshell"; +} + bundle agent main # @brief Inventory full disk encryption status # @inventory Full disk encryption enabled - Whether all non-virtual mounted filesystems use dm-crypt encryption (yes, partial, or no). -# @inventory Full disk encryption method - The encryption type(s) in use, e.g. LUKS2, LUKS1, PLAIN, or none. +# @inventory Full disk encryption methods - The encryption type(s) in use, e.g. LUKS2, LUKS1, PLAIN. # @inventory Full disk encryption volumes - List of mountpoints backed by encrypted devices, e.g. /. # @inventory Unencrypted volumes - List of mountpoints on non-virtual block devices that are not encrypted, e.g. /boot, /boot/efi. +# @inventory Full disk encryption volume ciphers - The active dm-crypt cipher per volume, e.g. / : aes-xts-plain64. +# @inventory Full disk encryption keyslot info - LUKS keyslot cipher and PBKDF per volume, e.g. / : 0:aes-xts-plain64/argon2id. { + vars: + linux:: + "_dmsetup" string => "/sbin/dmsetup"; + "_cryptsetup" string => "/sbin/cryptsetup"; + classes: linux:: + "_have_dmsetup" + expression => isexecutable("${_dmsetup}"); + "_have_cryptsetup" + expression => isexecutable("${_cryptsetup}"); + # Flag each dm device that has a CRYPT uuid "_dm_is_crypt_${_dm_devices}" expression => regcmp("CRYPT-.*", "${_dm_uuid[${_dm_devices}]}"); + # Classify crypt type per device + "_dm_is_luks2_${_dm_devices}" + expression => strcmp("LUKS2", "${_dm_crypt_type[${_dm_devices}]}"); + "_dm_is_luks1_${_dm_devices}" + expression => strcmp("LUKS1", "${_dm_crypt_type[${_dm_devices}]}"); + # Classify each mount: real block device? (starts with /dev/, not a loop device) "_is_real_block_${_mnt_idx}" expression => regcmp("/dev/(?!loop)\S+", "${_mnt_data[${_mnt_idx}][0]}"); @@ -25,6 +59,11 @@ bundle agent main expression => regcmp("(${_crypt_paths_regex})", "${_mnt_data[${_mnt_idx}][0]}"), if => canonify("_is_real_block_${_mnt_idx}"); + # LUKS1: flag enabled keyslots (slots 0-7, all share global cipher, all use PBKDF2) + "_luks1_slot_enabled_${_dm_devices}_${_luks1_slots}" + expression => regcmp("(?s).*Key Slot ${_luks1_slots}: ENABLED.*", "${_luks1_dump[${_dm_devices}]}"), + if => canonify("_dm_is_luks1_${_dm_devices}"); + # Summary classes "_has_encrypted" expression => isgreaterthan(length(_encrypted_mountpoints), 0); @@ -64,6 +103,14 @@ bundle agent main string => regex_replace("${_dm_uuid[${_dm_devices}]}", "^CRYPT-([^-]+)-.*", "\1", ""), if => canonify("_dm_is_crypt_${_dm_devices}"); + # Underlying block device for each crypt device (for cryptsetup luksDump) + "_dm_slaves[${_dm_devices}]" + slist => lsdir("/sys/block/${_dm_devices}/slaves", "[a-z].*", false), + if => canonify("_dm_is_crypt_${_dm_devices}"); + "_dm_slave_dev[${_dm_devices}]" + string => "/dev/${_dm_slaves[${_dm_devices}]}", + if => canonify("_dm_is_crypt_${_dm_devices}"); + # Parse /proc/mounts into indexed array # Columns: 0=device, 1=mountpoint, 2=fstype, 3=options, 4=dump, 5=pass "_n_mnt_lines" @@ -83,54 +130,191 @@ bundle agent main canonify("_is_encrypted_${_mnt_idx}") ); + # Map dm device to its mountpoint via cross-iteration + "_dm_mountpoint[${_dm_devices}]" + string => "${_mnt_data[${_mnt_idx}][1]}", + if => and( + canonify("_dm_is_crypt_${_dm_devices}"), + regcmp("(/dev/mapper/${_dm_name[${_dm_devices}]}|/dev/${_dm_devices})", + "${_mnt_data[${_mnt_idx}][0]}")); + # Derive unencrypted mountpoints as the difference "_all_real_mountpoints" slist => getvalues(_all_real_mountpoint); "_encrypted_mountpoints" slist => getvalues(_encrypted_mountpoint); "_unencrypted_mountpoints" slist => difference(_all_real_mountpoints, _encrypted_mountpoints); - # Inventory: full encryption (encrypted volumes exist, no unencrypted ones) - _has_encrypted.!_has_unencrypted:: - "fde_enabled" - string => "yes", - meta => { "inventory", "attribute_name=Full disk encryption enabled" }; + # --- Active cipher via dmsetup table --- + _have_dmsetup:: + # dmsetup table format: "0 crypt " + "_dm_active_cipher[${_dm_devices}]" + string => regex_replace( + execresult("${_dmsetup} table ${_dm_name[${_dm_devices}]}", "noshell"), + "^\d+\s+\d+\s+crypt\s+(\S+)\s+.*$", "\1", ""), + if => canonify("_dm_is_crypt_${_dm_devices}"); - # Inventory: partial encryption - _has_encrypted._has_unencrypted:: - "fde_enabled" - string => "partial", - meta => { "inventory", "attribute_name=Full disk encryption enabled" }; + # --- LUKS2 keyslot info via cached JSON metadata --- + _have_cryptsetup:: + "_luks2_cache[${_dm_devices}]" + string => "$(sys.statedir)/inventory_fde_luks2_${_dm_devices}.json", + if => canonify("_dm_is_luks2_${_dm_devices}"); + + "_luks2_cache_mtime[${_dm_devices}]" + string => filestat("${_luks2_cache[${_dm_devices}]}", "mtime"), + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + fileexists("${_luks2_cache[${_dm_devices}]}")); + + # --- LUKS1 keyslot info via text parsing --- + _have_cryptsetup:: + "_luks1_slots" slist => { "0", "1", "2", "3", "4", "5", "6", "7" }; + + "_luks1_dump[${_dm_devices}]" + string => execresult("${_cryptsetup} luksDump ${_dm_slave_dev[${_dm_devices}]}", "noshell"), + if => canonify("_dm_is_luks1_${_dm_devices}"); + + # LUKS1 global cipher: "Cipher name" + "Cipher mode" + "_luks1_cipher_name[${_dm_devices}]" + string => regex_replace("${_luks1_dump[${_dm_devices}]}", "(?s).*Cipher name:\s+(\S+).*", "\1", ""), + if => canonify("_dm_is_luks1_${_dm_devices}"); + "_luks1_cipher_mode[${_dm_devices}]" + string => regex_replace("${_luks1_dump[${_dm_devices}]}", "(?s).*Cipher mode:\s+(\S+).*", "\1", ""), + if => canonify("_dm_is_luks1_${_dm_devices}"); + + # Build per-keyslot summary for each ENABLED slot + "_luks1_ks_entry[${_dm_devices}][${_luks1_slots}]" + string => "${_luks1_slots}:${_luks1_cipher_name[${_dm_devices}]}-${_luks1_cipher_mode[${_dm_devices}]}/pbkdf2", + if => and( + canonify("_dm_is_luks1_${_dm_devices}"), + canonify("_luks1_slot_enabled_${_dm_devices}_${_luks1_slots}")); + + "_luks1_ks_entries[${_dm_devices}]" + slist => getvalues("_luks1_ks_entry[${_dm_devices}]"), + if => canonify("_dm_is_luks1_${_dm_devices}"); + + "_dm_keyslot_info[${_dm_devices}]" + string => join(", ", sort("_luks1_ks_entries[${_dm_devices}]", "lex")), + if => canonify("_dm_is_luks1_${_dm_devices}"); - # Inventory: no encryption - linux.!_has_encrypted:: + # --- Inventory attributes --- + + linux:: "fde_enabled" - string => "no", + string => ifelse("_has_encrypted.!_has_unencrypted", "yes", + "_has_encrypted._has_unencrypted", "partial", + "no"), meta => { "inventory", "attribute_name=Full disk encryption enabled" }; - # Method and volume details - _has_encrypted:: "fde_method" - string => join(", ", unique(getvalues(_dm_crypt_type))), - meta => { "inventory", "attribute_name=Full disk encryption method" }; + slist => unique(getvalues(_dm_crypt_type)), + meta => { "inventory", "attribute_name=Full disk encryption methods" }; + + _has_encrypted:: "fde_volumes" slist => unique(_encrypted_mountpoints), meta => { "inventory", "attribute_name=Full disk encryption volumes" }; - linux.!_has_encrypted:: - "fde_method" - string => "none", - meta => { "inventory", "attribute_name=Full disk encryption method" }; - _has_unencrypted:: "unencrypted_volumes" slist => unique(_unencrypted_mountpoints), meta => { "inventory", "attribute_name=Unencrypted volumes" }; + # Build per-volume cipher and keyslot strings with mountpoint prefix + _have_dmsetup:: + "_volume_cipher_entry[${_dm_devices}]" + string => "${_dm_mountpoint[${_dm_devices}]} : ${_dm_active_cipher[${_dm_devices}]}", + if => and( + canonify("_dm_is_crypt_${_dm_devices}"), + isvariable("_dm_mountpoint[${_dm_devices}]")); + + _have_cryptsetup:: + "_keyslot_info_entry[${_dm_devices}]" + string => "${_dm_mountpoint[${_dm_devices}]} : ${_luks2_ks_${_dm_devices}[keyslots]}", + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + isvariable("_dm_mountpoint[${_dm_devices}]"), + isvariable("_luks2_ks_${_dm_devices}[keyslots]")); + + "_keyslot_info_entry[${_dm_devices}]" + string => "${_dm_mountpoint[${_dm_devices}]} : ${_dm_keyslot_info[${_dm_devices}]}", + if => and( + canonify("_dm_is_luks1_${_dm_devices}"), + isvariable("_dm_mountpoint[${_dm_devices}]")); + + _has_encrypted._have_dmsetup:: + "fde_volume_cipher" + slist => getvalues(_volume_cipher_entry), + meta => { "inventory", "attribute_name=Full disk encryption volume ciphers" }; + + _has_encrypted._have_cryptsetup:: + "fde_keyslot_info" + slist => getvalues(_keyslot_info_entry), + meta => { "inventory", "attribute_name=Full disk encryption keyslot info" }; + + files: + _have_cryptsetup:: + # Delete LUKS2 JSON cache if older than 24 hours + "${_luks2_cache[${_dm_devices}]}" + delete => _tidy, + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + fileexists("${_luks2_cache[${_dm_devices}]}"), + isgreaterthan( + format("%d", eval("$(sys.systime) - ${_luks2_cache_mtime[${_dm_devices}]}")), + "86400")); + + commands: + _have_cryptsetup:: + "${_cryptsetup}" + arglist => { "luksDump", + "--dump-json-metadata", + "${_dm_slave_dev[${_dm_devices}]}", + ">", "${_luks2_cache[${_dm_devices}]}" }, + contain => _in_shell, + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + not(fileexists("${_luks2_cache[${_dm_devices}]}"))); + + methods: + _have_cryptsetup:: + # Parse LUKS2 JSON and return keyslot summary via bundle_return_value_index + "luks2_${_dm_devices}" + usebundle => luks2_keyslot_info("${_luks2_cache[${_dm_devices}]}"), + useresult => "_luks2_ks_${_dm_devices}", + if => and( + canonify("_dm_is_luks2_${_dm_devices}"), + fileexists("${_luks2_cache[${_dm_devices}]}")); + reports: !linux.verbose_mode:: "$(this.promise_filename): $(this.namespace):$(this.bundle) is currently only instrumented for Linux. Please consider making a pull request or filing a ticket to request your specific platform."; } +bundle agent luks2_keyslot_info(cache_file) +# @brief Parse LUKS2 JSON metadata and return keyslot summary +{ + vars: + "_json" + data => readjson("${cache_file}"); + + "_ks_idx" + slist => getindices("_json[keyslots]"); + + # Build per-keyslot summary: ":/" + "_ks_entry[${_ks_idx}]" + string => "${_ks_idx}:${_json[keyslots][${_ks_idx}][area][encryption]}/${_json[keyslots][${_ks_idx}][kdf][type]}"; + + "_ks_entries" + slist => getvalues(_ks_entry); + + "_keyslots" + string => join(", ", sort(_ks_entries, "lex")); + + reports: + "${_keyslots}" + bundle_return_value_index => "keyslots"; +} + body file control { namespace => "default"; diff --git a/inventory/inventory-fde/test-encrypted-volume.sh b/inventory/inventory-fde/test-encrypted-volume.sh new file mode 100755 index 0000000..bf6bb75 --- /dev/null +++ b/inventory/inventory-fde/test-encrypted-volume.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Create or tear down a test LUKS2 encrypted volume for inventory-fde testing. +# +# Usage: +# sudo ./test-encrypted-volume.sh setup # Create and mount test volume +# sudo ./test-encrypted-volume.sh teardown # Unmount and remove test volume +# +# The test volume uses a loopback device with a hardcoded passphrase ("testpass") +# and mounts at /mnt/fde-test. Requires cryptsetup and root privileges. + +set -euo pipefail + +IMG="/tmp/fde-test.img" +LOOP="/dev/loop100" +NAME="fde-test" +MNT="/mnt/${NAME}" +PASS="testpass" + +setup() { + if [ -e "/dev/mapper/${NAME}" ]; then + echo "Test volume already exists at /dev/mapper/${NAME}" + exit 1 + fi + + echo "Creating 100MB disk image..." + dd if=/dev/zero of="${IMG}" bs=1M count=100 status=progress + + echo "Setting up loop device ${LOOP}..." + losetup "${LOOP}" "${IMG}" + + echo "Formatting as LUKS2..." + echo -n "${PASS}" | cryptsetup luksFormat --type luks2 "${LOOP}" --key-file=- + + echo "Opening LUKS volume as ${NAME}..." + echo -n "${PASS}" | cryptsetup open "${LOOP}" "${NAME}" --key-file=- + + echo "Creating ext4 filesystem..." + mkfs.ext4 -q "/dev/mapper/${NAME}" + + echo "Mounting at ${MNT}..." + mkdir -p "${MNT}" + mount "/dev/mapper/${NAME}" "${MNT}" + + echo "" + echo "Test volume ready. Verify with:" + echo " cf-agent -KIf ./inventory-fde.cf --show-evaluated-vars=inventory_fde" + echo "" + echo "Tear down with:" + echo " sudo $0 teardown" +} + +teardown() { + echo "Tearing down test volume..." + + if mountpoint -q "${MNT}" 2>/dev/null; then + echo "Unmounting ${MNT}..." + umount "${MNT}" + fi + + if [ -e "/dev/mapper/${NAME}" ]; then + echo "Closing LUKS volume ${NAME}..." + cryptsetup close "${NAME}" + fi + + if losetup "${LOOP}" &>/dev/null; then + echo "Detaching loop device ${LOOP}..." + losetup -d "${LOOP}" + fi + + if [ -f "${IMG}" ]; then + echo "Removing disk image ${IMG}..." + rm -f "${IMG}" + fi + + if [ -d "${MNT}" ]; then + rmdir "${MNT}" 2>/dev/null || true + fi + + # Clean up cached LUKS2 JSON metadata + rm -f /var/cfengine/state/inventory_fde_luks2_*.json + + echo "Teardown complete." +} + +case "${1:-}" in + setup) + setup + ;; + teardown) + teardown + ;; + *) + echo "Usage: sudo $0 {setup|teardown}" + exit 1 + ;; +esac