From a3b571bcac05aac17371e9fdf254a0b7632600f8 Mon Sep 17 00:00:00 2001 From: ARISAWSS Date: Tue, 28 Jul 2026 09:42:41 +0200 Subject: [PATCH 1/3] Add dedicated security audit workflow. Separate SAST tooling from the install CI by adding Bandit and Semgrep helpers, a baseline-ready workflow, and artifact reports for iterative security review. Co-authored-by: Cursor --- .github/workflows/security_audit.yml | 85 +++++ security_audit/README.md | 46 +++ .../_sast_json_common.cpython-312.pyc | Bin 0 -> 2879 bytes .../__pycache__/bandit_json.cpython-312.pyc | Bin 0 -> 14717 bytes .../__pycache__/semgrep_json.cpython-312.pyc | Bin 0 -> 16434 bytes .../test_sast_json.cpython-312.pyc | Bin 0 -> 13236 bytes security_audit/_sast_json_common.py | 59 ++++ security_audit/bandit_json.py | 263 ++++++++++++++++ security_audit/baseline/README.md | 18 ++ security_audit/requirements.txt | 3 + security_audit/semgrep_json.py | 293 ++++++++++++++++++ security_audit/test_sast_json.py | 229 ++++++++++++++ 12 files changed, 996 insertions(+) create mode 100644 .github/workflows/security_audit.yml create mode 100644 security_audit/README.md create mode 100644 security_audit/__pycache__/_sast_json_common.cpython-312.pyc create mode 100644 security_audit/__pycache__/bandit_json.cpython-312.pyc create mode 100644 security_audit/__pycache__/semgrep_json.cpython-312.pyc create mode 100644 security_audit/__pycache__/test_sast_json.cpython-312.pyc create mode 100644 security_audit/_sast_json_common.py create mode 100644 security_audit/bandit_json.py create mode 100644 security_audit/baseline/README.md create mode 100644 security_audit/requirements.txt create mode 100644 security_audit/semgrep_json.py create mode 100644 security_audit/test_sast_json.py diff --git a/.github/workflows/security_audit.yml b/.github/workflows/security_audit.yml new file mode 100644 index 0000000..2422bfd --- /dev/null +++ b/.github/workflows/security_audit.yml @@ -0,0 +1,85 @@ +name: Security Audit + +on: + push: + branches: + - master + - ci-security + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + sast: + name: Bandit and Semgrep + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install SAST dependencies + run: | + python -m pip install --upgrade pip + pip install -r security_audit/requirements.txt + + - name: Run Bandit + run: | + mkdir -p security_audit/out + bandit -r TVWeb/app TVSecure TVConnections \ + -f json \ + -o security_audit/out/bandit.json || true + + - name: Run Semgrep + run: | + semgrep scan \ + --config auto \ + --metrics off \ + --json \ + --output security_audit/out/semgrep.json \ + TVWeb/app TVSecure TVConnections || true + + - name: Render readable reports + run: | + python security_audit/bandit_json.py normalize security_audit/out/bandit.json -o security_audit/out/bandit.normalized.json + python security_audit/semgrep_json.py normalize security_audit/out/semgrep.json -o security_audit/out/semgrep.normalized.json + python security_audit/bandit_json.py report security_audit/out/bandit.json -o security_audit/out/bandit.md + python security_audit/semgrep_json.py report security_audit/out/semgrep.json -o security_audit/out/semgrep.md + + - name: Compare with baseline when available + run: | + if [ -f security_audit/baseline/bandit.json ]; then + python security_audit/bandit_json.py diff \ + security_audit/baseline/bandit.json \ + security_audit/out/bandit.json \ + -o security_audit/out/bandit_diff.md \ + --fail-on-new + else + echo "No Bandit baseline found; diff skipped." + fi + + if [ -f security_audit/baseline/semgrep.json ]; then + python security_audit/semgrep_json.py diff \ + security_audit/baseline/semgrep.json \ + security_audit/out/semgrep.json \ + -o security_audit/out/semgrep_diff.md \ + --fail-on-new + else + echo "No Semgrep baseline found; diff skipped." + fi + + - name: Run helper tests + run: | + python -m unittest security_audit/test_sast_json.py -v + + - name: Upload SAST artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: security-audit-reports + path: security_audit/out/ diff --git a/security_audit/README.md b/security_audit/README.md new file mode 100644 index 0000000..7335ccf --- /dev/null +++ b/security_audit/README.md @@ -0,0 +1,46 @@ +# Outils SAST JSON - Bandit & Semgrep + +Inspiration : scripts `sort_json.py` / `compare_json.py` (Martial Mancip) - +**normaliser -> comparer -> rapporter**. Transposes aux JSON Bandit / Semgrep. + +## Outils + +| Script | Role | +|--------|------| +| `bandit_json.py` | Bandit uniquement | +| `semgrep_json.py` | Semgrep uniquement | +| `_sast_json_common.py` | Helpers partages | +| `test_sast_json.py` | Tests unitaires | + +## Sous-commandes communes + +| Commande | Entree | Sortie | +|----------|--------|--------| +| `normalize` | `*.json` brut | JSON stable | +| `report` | `*.json` brut ou normalise | Markdown lisible | +| `diff` | deux `*.json` (A = baseline, B = nouveau) | Markdown + code retour CI | + +## Exemples + +```bash +python3 security_audit/bandit_json.py report bandit.json -o bandit.md +python3 security_audit/semgrep_json.py report semgrep.json -o semgrep.md + +python3 security_audit/bandit_json.py diff old.json new.json -o bandit_diff.md --fail-on-new +python3 security_audit/semgrep_json.py diff old.json new.json -o semgrep_diff.md --fail-on-new +``` + +## CI + +- `requirements.txt` installe les scanners Python. +- Les scripts Python ici sont non interactifs et n'utilisent que la stdlib. +- `diff --fail-on-new` echoue si le nouveau run introduit des findings absents de la baseline. +- `diff --fail-on-severity` permet de bloquer a partir d'un seuil. + +## Tests + +```bash +cd security_audit +python3 test_sast_json.py +python3 -m unittest test_sast_json -v +``` diff --git a/security_audit/__pycache__/_sast_json_common.cpython-312.pyc b/security_audit/__pycache__/_sast_json_common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84431b038d11bb71bd5fd90c44bceba9a1a3607d GIT binary patch literal 2879 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 literal 0 HcmV?d00001 diff --git a/security_audit/__pycache__/semgrep_json.cpython-312.pyc b/security_audit/__pycache__/semgrep_json.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47cfec09a626af7cdeeb2475c31b567b900408ae GIT binary patch literal 16434 zcmbt*eQX<7w%-gn{Qee2eaoUgEy|W;OO|6RwiDTwEIYQmwk*fd#-J(ANTy7Y%FNKQ zxYR<~rVX_=YO2{+(~X1BNKn&_+bDQ_h|(5~7Z2zLctt^JxNCZ<2LyQgM}z+3L`l1= zqG->(1gSo({T} zHp8<5-YxK~r0ZxaJgeZ{2G44`o-TuD4ZVYIpzZIf#%q~6y8K!@L9kT{f}y7Sr4Htw z@V)la-0P=gHbN3}$~mRW=~;vidXAZyVwqX@G#3nX&CWSHoh%ceS;p!2alQ#Z(@J- zUESMTo@-{9aY`v1;|;MselA;JXHZJ$l1XWtRJ1r46zH9l#a*(j8Mij_ujfYj#p+31`(65SCj=AoJLAwTx#*-`R--cme+&puG4>$8ZKSN*l zg}XQp$9LsO3WwzSUT=<7FvBPzgqz%W6C!sAUQUY;5=%`Znr)ZFcJm~!l9D2%l!GR( zDF}k2Xhnp)POv1fB;`_tUmMMF(%I2b=PVQAoihy212={Oz>pJ+9~iQoxzXj^$ue(+ zd@L}7KRD$J>~uM4Pk?hy`T|o7JIne4eAl;DsCJWd4qb35MJ30xq6z}vtf=%e0hdBl zOwNiV$3Zg)r*rdIw?F9h_`9zLXPE9uuIIqs-hKPKFQBoy=nHpF`S`1$3E;UbN4Y&A zVCd~EC|0`t!QeH{?e|?{+y!l6%TXn8bFf;81>%odL-goP&27`7>8532U|pk+?)^vC zV~St>BIb>sTe_ODlxGZ<4WK2qOobLc;HmfqCgBbdA*SWYK(yJ5mIs+FgW)A$c+I># zK@r&Bs)10Nh|yA|C?$#;BnP{qkC-PTWYPMl0!kZMsyvx$UJ+5S)p@2UXq!SRNqVv> zS`|@zKzyheC3ub0!-5>eM?g($K1K5imsFDUoL80@HLXM;_m_%LtSv$n?Xymz4%kYR zr^vTqUR8odpQrhNpbZfv4pq=!)hOXIj)=-@%$%s_cn`}1Qw11N1xuLqh=u~@3R9D_ z{rw_^*FJ%m9H>PKlWVj}9yP`(2c%oML#^48wvsl12RFQi~+ZB9>rnD5yn2 zJqmU}z^Nf{;)}II6xU3{Dsw~2*e3XbIGTF`!2*%dXx|IH6N-+#8;Ki}@q-tQnp0iG{(8&V0Liu{w5G&^4~<4kpe$7`s1~JS?;x zTsZnvqf47xKk+7;KAuXOyD~OMY$A3c`iqpdX5CQ!o6)%KBgb7w{Ca{FsyhV7F2S&S z;rR1aJax84`)(gxJQ%x}Hq<5ee%AM}Z@F%T6?Pp3ah-_pPWn?`chOM>wvy(S%qjj!>f?RtFUlG?5PGdMBL@J`Ql#_ROLDFO!wrr!ztuLn^l0Lv!eWkibhd3n|e6^rW8W5a`I2GCOC zcu{}R;}0=|EE_~;F-3cL4~ItEnOzqayfD4IJ6A5-32A+Ba|s9*h;@xE)^d+ZYw8m% z58CgyCl3fs`~HJ^t?9_R#U49;XLxBiJ}#8Eua$Qtj|!IVl(xGdiz~^mHuYl4c=giR zwQ?75y`>|i?IO(6z=%}X`H)ba{RZ@Jl!pJ96;Z2+aCEogywYOrT4wv54k`<2C4;=Kt{Ff=84Rt;?_ zs_prfTObc^`M-f>w^d##vLtFsEHe2J-L^b|>47l|mZ-$8^W+pUugJ5kEdMBJ#XJ=u zj}tNS@?(O4!$4JZLwNqcRhOWY7lV9MX-ZJ4q}IulTHXZXgRh93Is=9jOZhVh%sU+F`s6M4)P-buXxL3suHDGUZ@nA zszfQ)7b-=jqHXu&StVfw-ui2bfDuPR&bAA{HSx$Ul}-{ol7a9=m%j^njuR3uN;LCk z5Q;57apbns4t(Ucl-Oq+QfVd9E2MO~@}6Se66pSk`Wi}ijEp&B$ zzl&ISv`OPDA`2aP;hS%iQ_a)OQau0MK3-BS5d~dyPd#r1@mdw3B38Qg5M+v2;nb)b zVOt>1BH<}3lcHG=l}IJA2gS62Z5f2=@#&HkR?#8BR^e2j!*Vh{T|~_c!*Lz}gf%Ep zg(PCmrDxG{%M$3(Y2(HR54#_I#lkNtihT(M2T*`Avj-vAA_SuI5q2bUqYEPFe9Xcs zcov92NZiVvh`E)gnp8qB)guKJ;jW@_#yxt7bO7Xyf`f5Wd&uvZnV>yK!p*YY0)YM; z!v0-{FlyzFKqO0a4NBtBWGoP|x8fB`bBV^p>HFPlP5YP6t&l4nUz*kqy!Lfjs^atl zfzM7jiqU{<=^PxN1f0R-2B^-6*9a1K@G=~F2m{gtVwrzV-XTfimVqMX$bYSTn{+Ag z2#&C5E)lx!+9Q&aIdqLhq)pwSDJH<&m}QTITmHVDNK`XW5{HoUeOUI1JDBJY>N?kVv_G;Z&wV!baBR6>=sdF4dHivc;2IQm3}xyXGF5e%9qpOAmQ1bl zql0%3Cf*QgyVq)uE${uJ@AJNuYN7X7rlIAdxw~`8dZFQ^wT81Rd%x=YvhQ)d@bcMA z^^T9K?^Y*X5~_EtRUghZ=Jij1`JKaHGXD>yNvD5S3j_XaKcOIG%?Ni7X1q)HZx{;O>FMNujoDrRA&kFWXbc z#)SThtCrUnsdXKInR_j1T|?sdgW>zb$&2Zh93-h&IxSclqtrLn>h-d!jJ15-Rvx?b ze*Jo7?Y*JI{s)KdANu%+P}d_=_GZfKzB6fR)eFbIvl3ccrmZ`wOc|S2waqZq3#Xo! z`H^Y}HZ5}FDntrp{({p=2H|i*s)20VC7n*v6R2dMnkSXyN>HMc4;3T$pmM2LR32U! zENTfX+`w#zXJ3an!ipYtiYwUBMU!`icFPNky$oavaC2zCfCWXF(&mOwbjg;_Iv#ci z&OX7se}(!=`=vHb4P*?)Z12&rAzetJgL@BC2)#!*1Z13?j3*JHXI|+5Qe1Hg`J69F zp^T8ArlLNS@d{89#WcXQj4#;A61Az}d{m+11gZF>kuO=Z2B@_!Osy-?Dp?+sFwwTU zi}yvA{7d%52(2@`aO-}$x3bh;ay(>-8_fW1L3JM|F8?Purj>x*&>_CXEbUR%xrO#7&wQNMjrL^PPaaa&qK$8SrQA5y`VQ2}U;Nq>4Wm9Ms!i&YJ zswMuA+73v%I1ks9D4~{CNbLp=u8V-8m98&AyJHJ&LkTM97OKVw1tm6pDDw&kNMB>T zQEV+1a)3O0x0PPS6ya4|1|hWMEY?aRK{sCq9m=iBh;m7BtA--x)sROWBUw+0k!g`~ zmgK3zR;jVh;vjBG@sVyVF)s3q!g#cmpxGg{1!#7bpm7$^P#?+~gBNOj(H57e=ec$9 zfsBb>sIKB1+b*?U=2#7AAAVk9Zd?*|sTvEkPVz`h8@4TRc-@Y>N|i_dWR1YxInFIv zMn{Pel~*gw?$)!S=)4rx1StIwMbP$XSyPqoV;+Z&!YU!*o>#t2xOR<%Ynz?tL8wMH zTgiynp0i=QFtiF=JDnXJqrnjCWjZ=QBYb6(yn?0RunDD{0dRo7hi&L}D5qJ*pV*Vo zgs=mB|Cxscqc8>s*<#pJkjB1-2x<|*@nC4eA4JB;5P~IuIdKu2dm_-VY(8hid7ha9 zEp)^=Dl+ zc>es^^CC5T^2Bhss=3*D9LCM(2gd=N04@SJMb!*RSHe(UCmQH}jG)8HD_5?B&7Drk zEQMylrD4ya<~SihYW!Kh$IDr7K!8I9q+8}HnRsneRujMjA9xM+;cCoUx0 z?!TEBNjZAc+I@1y7WlsN{^i8klx<&{>XW~>C(A#pdRVpGDY#x;a}7N{nrc6hvJI!H zleyAU*0!`}XRi2+*_CWbbq}s~45eHrQpVvQlnM~=)r3lq8os5iwqt4PxKvN}#bk?h zteWb-C$t!WS**81i=o&Xf~8^2(wgXbV(wZscO|*yLBZVr#C&wseDv|YukC{Q4B#Y# zIm+EU5S#e*;Y^t$=8dtj_W0OpSyS{_CRfgzf~9HAaya36z}#n&r-Zhb;BBQRO3974 z#%y;SOO9AL(InVfp=Sv?xi8tatXlSb!F^hWs_1xO7bH5%)8O|>p zhd|UAa*awky3^V{((Ij2YpcNMxIvh8_0fTh(RTav;^|CTWt{v-bypSl3st*PmAjLJ z%Z*>Oe%`vG{>#qghR5XN#^1I6w)JbHaC9s+a4~i9^_2AzOpM746JshvMa0X}Mlglq z;^G4s7rKPH-D`C{%ZgNOZ`#-=C1S#g$~(*w6F>2OV66h|InfhOEfpE7RRH(t*E=e9lc#a*1MA(gx zf&=3?_U9;u9Lewwku{uSP^VzuL|=Ib3RPU99wEEjrV@d~zd{5^9Rsk=P zKMeCD7;!g;A2$sQF*^Ow`u;m+WlbW4wna11gx+@Lc; z2LE3I{lAAhr(w)AwkD=m8~6Uf&=Vhs4nh1OfwKYReit70@Jg}B7YO31G8)!sv zFa8D0|9j^DPwtS%Cy(`Et`Q>Nc1av1J;7R+x8vpQefe*)^fOP*D+~BdM!p5#Wb6Yn zxT?U$1~$Af^jEG^Q;?>LsJEnTvCTmm^i0mDn<9Q~UNjf8$Q7dc5)^VzNTLJoM_)=Tt8SCCV-cD+zeeL+sy z()mI;3&xnPikc@$aPu_I>lH*qPa7i|K*zxI=pqUH%@ol_l=*LY`zCFkmMxsw94K6r z(LRiCMgF@K%kN~g1+1LJG$6?B7fS*DP|#MKUb8(Fwt|7Y)eA}}LLu`b^aykfJ7Kpm z7385JCZvU!mhDFQI%r!SD@&zBjMrcv1q9oQ+C&lok|Od}CAzF&E(+$KwikRam|eQO z;CtcRsd=PX!`F0+6w5y!Ir3u{F~Im#jDp4j>@?LQVKd0L$dfY%DY8JK0*U!YJsg89 zO3wBHR~Al$L0|^=Sqv3)$Q2QLreskAcyS^(R%iQBi5P>7kznXL;|bl!;tmW@&NE!l zAIf5e9Ax1=;E0?3;?408Zavcb*^$=sH*m)N1$l>9)D+kHML@qsD+=~UdAEb_$}MZU zcnR|E1~`!9noHDExHtS{Idi32tCqL}c9`O&$h?4B16;V0)lfvRBNJ$usG?dcnNxCT zoSK&9iaamSI$AGrL(y~tZIsespO;ObznKsQ(q3J#!P0tH?a&?vyb^$^6q1r-#*PN4wF zJ?vE!V0*0(#imh!Ew|TD4DEZY9|bcIxTq|;M25Ajk?t+@RV%7^ACI6Sh&3G8$yl#Q zUc=%KdmWL!jRI6}A$nZMqDK{8kl{)s>|t+i*$Rsj)=;h^aCrX-pkxjTa`C?S&P3TK z$CA{?ClfCxJt@bYw05tQjIhR2i(QhvYtzP7DV{|OWimuLk=71xeX%*dCv-Y9U=*u8 zHn60Ih`szy-%?+^TCg{-*}+%rNmxgRrSYrQFIyk0AG1RLsno&OzTPJn1Eh9Ws;mRhn>RVO+ywZ|wjb~tpd%CX z;8{5V{-6jspZ~qK3fnot4bm3rY{wqnM|V3t6C4xZIrN9XfGfNsw_)be2ezaibzxQ- zx`5dwT$VHa;#uoQUipW^didc1cHHX>eK@Rjf)^m{wTB;q)n*cWzp>Ln*2tq}-$g8e z_FNX}3c@<%7Wi|}OX=g0>gj z;d7r(-#;zX>=m><@X3Tsz3zMd96gG7a>yqYWdS&m*>tFV-?0)P^*?yL$&dD?_ zpNuB$A74tGO4+*8)E@bJGwcQ0kIy7tN!fbSR9-oRzNy z*8xh7lzvxobh!zXM^7nTO5c$jUY|1TvCz$)dnr1y4q66zth9a@|zUqPQ zzAo9ZJRvv_3YPvAuV5L-RMy6*r&h-^4Uqnzhc;#Ob)bZ>e{d;&Dlz?U?aP<`vVPgJ za^pWMzaIJNN1Hycbv@BkN2%?xS zMxm!cRCr{{3-~~1$d@3+cv&g&NEYysSW{H^u+Y|mwjm>5v6>6pR-EGbwkeCY4Q+P# zdV#iq(XgOxNC+#|wgLyc5)|cm@vj8M^Q|vlmxHc&R4L6jjA?PID)@RchFIqPoFW4y24W|MCiFW*}8;^gjJ9g651^xjB z_~*?FKPI8y;Y20ulovqFPQgbWZNB(7Cp!Co7yN}6X2MonNp>@K!XHfx@-eu|_-C=c zDy--XhE<)N;MlWCc0!VdW+px08jq+ZLjf;3jZlliRg%5rjEB8O2j51&SmA&EkQ4mm zWVqs3{$Ib)4W}pI@y~G#$Np0o2KF`tU^glJhYkIx5@g{wGd6^pr1p4GkHTenBI$4v zRi_6p4xSG0l1CmsH3`)+mD1M;`o4u@-)IfDLAMk8MauPZ z)GTNZEevER)q5lFjKp3_Q?*ijU%WEeyfA`{4cXLtCiK=OWxtVXdlh9Lc+u>7l)YKX zem&K42xUK<&)%P=c1X1!OjGqz{JECIUP&H8g&oLIWZ-8XYh}i?|CxqR*?%A?mHHW+ z5o*+@ds;-<2>Q*1;q0~#!zEwxwvVLf!fpT497!?1hWww1^Q4Z*|5{dj8_oZPC~Y;P zL^Hu7jWxbnz@@LylbS=jnl=2V+96-5)i6q{DpNi)dzf{aAv(L9d=Xyi= z1_5r8)JRx^IlSM=E<)~aQ6`rbGmogpY>F5ZAPM}#$o?xx6m^3)ybRtQ*nbA^?E5JA z4a%m(zD3t_vK+%9I122UWln0w;|s8V4WDXZfVokKAT4y|%_C0?Evp7FLJ5YBh2zf@ zl*)!Mz`i>Nmk!1+yboIF=9ISiPwrP~AuTm2RL-`5$)-G%A1FsDh}db-Pgu+-`tiNRO8loo_)*q$a@c5-Z{u(aFBKKDM%Rq+RXR%~nIx`D?F$fL536F4@w0m@oc)&?` z7Fir|RwSCb)LC6P%D{$D@LbVI@sA74=DLY;DY9*>l#3qNvM;&F5vcvQKpD2m$N|i% z2oe<3y+hQv-E`3Fc8hwqdol#LfpNQ8G_&mIC_u9>8j$ZNgZEbsJC2HKer^_igu;U* z+T7^hUi@P)xU0_01Or(QH6-t3olw!)oV9GYPz-t5Vs};Sy^6h3vmgcere%#EcxOcV zf4n_ne#Oj%<~aa^2o0u~8$KSJ6Y#pmYzXelGeH`RvPane0-4cL=g@Eg^drgdC>=@t zSV54@e@9sUE1~~eqTz3eivLNeNyU!@1Ypq9cQ2?n)GE@tVbzoSH)?d`Fu75sBzJBY z_mh?lmQ;|Y4SgqBw{e98Yv6{FBzrfsFT;C;tR+o998!}uD26gd!)waw3}sw6_TKP2 U!;2SgUi>{(4&IMc4Q}lJ1x(Ok@&Et; literal 0 HcmV?d00001 diff --git a/security_audit/__pycache__/test_sast_json.cpython-312.pyc b/security_audit/__pycache__/test_sast_json.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f40e4950c1d740eb40327439c770d9348b7a246e GIT binary patch literal 13236 zcmeHOYj7Lab>7A90t*l%_!b``1&T6BP$Ec*A|=t1Y?7iLc0}4DWm|L{1jJpEpzvUJ z7m|pQ9@=rnbdss5IEkp%N$H8(QX_SuPBK&V@k4g~+`IRj``vTSUHyH2zKwxv;^m3SwmOFSH9i=ZT~Dy< z|7l{F%M4*g7{U@$fDM{POf06k05`(XJ3qqHyD%cqyLrS6cRpYViX$S+m>5Zvtk+nb z<7Ao9Mf!d%nVc+7=NYLHyJXLmpDT|D(L9DBW=btcNAjWf0?BUlF7cDK2IutsJS*?a zT`~#tl6?_&GtX-&O*sZDG`a4*hn7y*-1VrAP!Rau1JbVF)4w(Qc_0B z$u^p2)GLL06?(mK&d-jNkxEiUs>yazLu&nOlI|c*Qb%@@`geJ<>s^67K^mZ>k(MKE zA}%OzHp<6MWcRz=NIBV0_PoK5Y%?*(8Pf6wL)?;ivP~a-CcVb#Pa~DIW-HWedqmAD zm=p74S0)!dE#;GTl*Hu0t!0MXFS2q8%RtS&kE%&JjM=6m+Xo}-eE7(WK1OP!TGDl{ ziDBeoI@Ti(9uLb*bJLvbK=eDdVM$RHXC&lTy?$9zoGs2#Z;1F+&!iF#xu>R`txiP> zj?2=Nkz=!+QM}`lvx!Jk5x?T}Nlp@06u%_9oZU93b81?h2!}eHDsc8FULZ+U8D%X& z+9g)Ws^@GuZ~GWW?qan(ZzvR2y{bPPQU>6ef6l8;XtuU4&y*}h&P$4BAM=k(K`Eqq z)Uc%S*qdg7*N6mEMdN$B+B&r=MfJ)mv*fXgA;}Yp1V<%V zv*{I8>7uIfzA%B;?d@pWuUSU}Uf;QZAKu)2s{iQ8GpC|84}YD%!q=%gqe#y1m~#q6 zHyW6B`a_E3i^!65EE1yg5Vc6>y@95SuILkwcy&;P$doH8w7c8fZ5lT?G!#9iYk@o9 z4UM}gV|ZD&Hv*dFRwARx32dP=*P_Z1DR*wPI_p(nYK9~k<__ed@i;&J-vIDxLjUR0 zgQug;W8p}MIMY4oBBI?K9)@nB1+w&&2-WmJc-$X~mOmGsl0qo4J-QreHJ6Lm#FUII zLC_pfc`a`YI(>N(aGy%jVd!xe-nGWyy zu%EPy`a`5e4Yzm$ffnE{$zxv7SNFpTqcvL;H*<4+D88;m-Ty;BW4K|uHy zty03ib8gISYHCboHtumAXmhz-oMwRuR$$d>g;EG6N%G*N!CIAwECN$sazlV5!2pqk8n{aBK&p1$YFu&5aTire2g6f z5SU@yu;@2f+lCNP$65|tWi=5NPndvwVEncVu(l+RzE%)@G=W5dQ;N$Z<9?^{#H)H0 zWae~k^tT4WK5w9PA{>-j$CSMX+B^1jwGP8nk{A5ZmT_3)kTepClmgmts z#qQ)zVErZdDUCqpm;^7rIr!S(w~tT=I&sryE0thDo%Wo~elKat3D%ssmf zRT&==<1+T$^?i_YnUR0^BXrX276=VUSxd zh4g1&WWG${Sb!}R-8|%8X4F*KWTt?Vqft|WGi;4n7KDvFvMv;7CFH58{7KZBtSeKv z58z@#E`)n@%}LmQvoFFM<0P4i#1b=dJ<7ao@|h+;m0y83m^ID7tYZGROy9Kf%q#~v z%w&ExPZ*6jvw1v@t##BQBac|$U|=?qJsLZ;F)rf{yD^M|h2N~{0^_m`Xl4ZPd6fid6_jy6{Jic%s;GI$=@-d1GcMRCFL*t6Zos*`cjkxc1cV{7I#tsMA z+1=gMu}K$Dx*fQTk3r&Du=Q{$zXDSNQU!!;s6qbYD zfIljw-e2)#ObY0@2k}>NAEFCdIpR(+JO})hoj`uW{MQ`w^Xjv=+Lzd+=J)c~oCjjg z12;?K&LgYakHo6aeyZS1yuPE3by+pj=%iMZ?sm#M9SD1gX6x-4IC^r}bGqk6uu&96 zlGXmNM7#mbJk)oh|5T60jZ3N~>Ln*bnoUpP{-bdK@UXe?iWC^rc;qejL5CU-l2d5k zPPpiVXf2QR+R;P5&7={Wq4$BzF}EE>YmWMuqyGJ8;*QpNJ|WuI#EO_$u~@n|ai#Wp z@eeDnR<4R2nfc>aYL?kmu{k5}Pb#idtn54xuRM6OeN{Z1*x9fYjqTj~3rEM|^9$_4 z6Yq#Cj*fXA2sC;-|E>I$@~3W=YhyLf=ekAH!WMC$O1xcqB=9!=(ja zWq>_^flC;W(R!CLPu`9M0nWwC{g}oTDj!F30&_We0B)Ko)DFMB3cNq;YDo>M%+eq* zF@sZK752_()5EnTT{8#_mFIwfX5>58^6O&xb?-OF^IPUkspqAg@%$&|O=;;DFP>f% zote+iT&Z1VW0lRz-LcB9o92(ir)2b|Hpvg&bt*rU!=vP9z-cEylqx_=dS%K;O-Tsg zokiD6O>IQRe9v-FX2P;rUd36a*JLt0U_9fUaB2$dVG~Ghmbvl>pOKrf4UL;@7oY;GP1gInf}Tx@Q3c01dy=4m3hLsRI`XvM>*da_rZJ__cyPYuh{GLLQ1R?^iO zwOiwZUVljA6ji3`tyyKsOQ&@}P&iS51amj-JQp^&5sso=N^d#B&2WT21mlLgfHnRY0$Y%#W*DT!@A2?t#Tvv0sXR=nRv`4k6^*S7 zjx*dii+%&<)0(NZyrrgz*lCTFc{f?0^US2zk`I6`eA=L!Nv-BtK~1eqShe|xAb>qd z6rVM3DlMR;shwfg0xcXAbHt3fMIFw%NTIsT$buDZU`{&EV&svcjG8HYAjKJ&V+E|h zDa*QMtfa&kH{G+Uvns$NNI;yd$&p`rz04Rvn#W_{O?k#n_wm00_RGc4i;SY#Fm-oR z19C&??ml?%piILs;<27*`<(;*FUmOm@{>qj#MZSO(g^@Kwn$cwf;5V{`FVir3) z3hDzeR#LWB(ikggyl#t^bj*vli%Zvv8)C%`??>asd*>~PX)i90#LAkMi(+NFECT>qu?O3wLs+w0ScQ5Z~d4Q zupz*ItMKyV$(s`}5#?!J98G1>+6$VRV9bl`dX#JS=q>d)kAWs2Fw6NZJ+B=0c zoqC8xM&U<`kA@?o0SH<{EEU6O5GItr2nAPl+a-+%VCKlaG#todP-(0EQc3|qABHFB zmCupzg;2CU7Y1ZiT0nk30+MaLSgl=|i>tV1(Yw?Vt8M*pRlNAnd>@)G=PtFT&6gAJ z`a%b^u?T&ImVv7#6owj+LmEF24oMK*5xLzVZ z5AQFZK{Ad+MKS~ADnlI*3cB(E%teAgt<(Z}gIPCOgqPX%VqSQfUAK$EzV*@~;W&G* z!6tO+O^pUnUB8msKfAsMo--nLa9#)zIgXf<5j%4-VrSMPcGH5(qB&5YL;6|EmQWo! zycAUaoifg_8j@Y={p&P`MfQkL9ZA_HU;)6Pq(lRuzx6a)#an z!zM6lrELRPxCozF7g&Bte(9Rud>R5tVDAs;n>+@=bg!7&0sPn$m(i88AAK6`Z1!%j~2oRBV^KU_^O0rBVtGi;bZ+t{(A0 zD_m{>0%4ywF-^lh^O!>W(RY%Ta*<1qOM1&YAQ7Jr_fsY zOCY#w7y+N%@qG8g13t&ER4M!i>#k%?D=Of>E>F0KL z&dhrPXSLlCnWpCD=J#Ixg`;b!4x)t3@8qvIx-d%EmEyTrx@1`{U)j<2k=Xv}CsroE z^;6C6{`6lQT?#r6A5_|VcZ(mm?Y(Wb4@G-#=jIq;D#rA=Hw=@7{_^MEFiu4B`hVjM z%YGP9^3cSCZl&ym47%gdLrnP`KGMCg(C~?!$?Z=@S3T_;%K?1+Opb9D(gd+#2nlXh zkL4E2Q_v!pgG(*2XnRgLqT#m(_^>ySUtg9Fn-?$Hm+8a)LA)6J$(%lHZC%nYMIY7{ ztJ=L%xo5d|IdbELtFz1Dn`c(G^(8&n>J~`&!6p4IJ=nqDg9nQtLf;RASA(}+Tn@(F zr&jF)=)w+C7j}@ku!HP|9D&K)6eLV}t*?LXwZhzx--*}y7#^u@AZky3c%{d+F2bnf zX(TkRmF0;Rr9IK43lS|#e+85AosBCm=PIUl1zr4EVJz zqMN~4&gf6DJ=zx8&ZuZTxWyTD$74EJ+eZzuO*r_g%@+zCWZXFoooM z@KZ2&i_WDWG_My}g}v+LMZzF>l~&=P-iB7c0JS#Ck{SS?i$O>czo5dCnP^S&^EYSa z*Kbbmr~)xZN%29;)MXgc>)RnK!y$zaTB$?I>kcV=3@B*j2+2L;aQvdEU+|9hpVvP~ z+XNFS>#0dcM^8BuW_4EfZ#Ub>?hKJz5*F%~W;3Y8sCAbOM?h(>Wh*uwtY>Hae>^bdK4 zdWMFx*JD4_kvFfxt4U+O4vR*SNJwZ1S%6g9*4OmIuH-kI_>qAJ_tfZ~+_KddYIg09 zfTe=kjNWPWVld{|JL-5(b6-L7B_uqOT-g6zD9G|9wQ@ih_^dbiNpM=hS{|lg zTdLni&S-;B4O^-0j5#|0?^H})gr1XlE@b2RWhj&X2uT)-e+%=F`~&>xi7TMET@V`X z6#&tp8;db0iD%dzkISrE>U4rTu*e)9orJS*`BiwJ^X_tL7LSL7eR>FUECS(82w}>I zqH;Sn;ozHMDlARJue5RcH4A>{6!4Ey7T6!2umcDD^f??jFKOl}FCIW?mZ84WC!ZT0 z(nbDdWQrJqA5h{~OIkAMO~25lhhmyxbm&=BD~@9`Y9MrCTM;GQ2WMS+&=hwoa8u6t zr_O`|@SUt?@k5{t#zW^ypIwS(o2PR_Jfz2KkI2`71MDV@vH|oC$Fl5Q-pumSIp*`vm_klZ@>E1t80#3F~_c@ znV6&FMkwYudh2}5aca)~kDPh#!dDM1D3>o@x)|ejBzVi4Ctf>&pJXodTv8VIzcaJS z@A@@U@k^!>A`n8sueQ}BDmxRkyAsZZM8&~Gbxoq`$wcX{yG2%rQIs`w6Y>lWT* zx@XI08x|oh>5kR7*BN+N&x7ncn0?2|@b>xW&$-G3Uo>~(%~P+P`r6 Any: + p = Path(path) + with p.open(encoding="utf-8") as f: + return json.load(f) + + +def dump_json(data: Any, path: str | Path | None) -> None: + text = json.dumps(data, indent=2, ensure_ascii=False, sort_keys=True) + "\n" + if path is None or path == "-": + sys.stdout.write(text) + else: + Path(path).write_text(text, encoding="utf-8") + + +def write_text(text: str, path: str | Path | None) -> None: + if not text.endswith("\n"): + text += "\n" + if path is None or path == "-": + sys.stdout.write(text) + else: + Path(path).write_text(text, encoding="utf-8") + + +def norm_path(path: str) -> str: + """Chemin stable pour comparaison (sans ./ initial).""" + p = path.replace("\\", "/").strip() + while p.startswith("./"): + p = p[2:] + return p + + +def sev_rank_bandit(sev: str) -> int: + return {"HIGH": 3, "MEDIUM": 2, "LOW": 1, "UNDEFINED": 0}.get( + (sev or "").upper(), 0 + ) + + +def sev_rank_semgrep(sev: str) -> int: + # Semgrep: ERROR / WARNING / INFO (parfois HIGH/MEDIUM via metadata) + s = (sev or "").upper() + return { + "ERROR": 3, + "HIGH": 3, + "WARNING": 2, + "MEDIUM": 2, + "INFO": 1, + "LOW": 1, + }.get(s, 0) diff --git a/security_audit/bandit_json.py b/security_audit/bandit_json.py new file mode 100644 index 0000000..85a0d13 --- /dev/null +++ b/security_audit/bandit_json.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +""" +bandit_json.py - rendre lisible / normaliser / comparer des sorties Bandit JSON. + +Inspiration : sort_json.py / compare_json.py (encadrant) - pattern normaliser puis comparer, +adapte aux findings Bandit (pas nodes.json TiledViz). + +Exemples : + ./bandit_json.py normalize bandit.json -o bandit.normalized.json + ./bandit_json.py report bandit.json -o bandit.md + ./bandit_json.py diff old.json new.json -o diff.md --fail-on-new +""" + +from __future__ import annotations + +import argparse +import sys +from collections import Counter +from typing import Any + +from _sast_json_common import ( + dump_json, + load_json, + norm_path, + sev_rank_bandit, + write_text, +) + + +SCHEMA = "tiledviz.sast.bandit.normalized.v1" + + +def finding_key(r: dict[str, Any]) -> str: + if r.get("key") and "test_id" in r and "issue_severity" not in r: + return str(r["key"]) + return ( + f"{r.get('test_id', '')}::" + f"{norm_path(str(r.get('filename', '')))}::" + f"{r.get('line_number', '')}" + ) + + +def extract_results(data: Any) -> list[dict[str, Any]]: + if isinstance(data, dict) and data.get("schema") == SCHEMA: + return list(data.get("findings") or []) + if isinstance(data, dict) and "results" in data: + return list(data["results"] or []) + if isinstance(data, dict) and "findings" in data: + return list(data["findings"] or []) + if isinstance(data, list): + return list(data) + raise ValueError("JSON Bandit attendu : objet avec cle 'results' (ou liste de findings)") + + +def normalize_payload(data: Any) -> dict[str, Any]: + if isinstance(data, dict) and data.get("schema") == SCHEMA: + return data + + results = extract_results(data) + findings: list[dict[str, Any]] = [] + seen: set[str] = set() + collapsed = 0 + for r in results: + key = finding_key(r) + if key in seen: + collapsed += 1 + continue + seen.add(key) + findings.append( + { + "key": key, + "test_id": r.get("test_id"), + "test_name": r.get("test_name"), + "severity": ( + r.get("issue_severity") or r.get("severity") or "" + ).upper(), + "confidence": ( + r.get("issue_confidence") or r.get("confidence") or "" + ).upper(), + "filename": norm_path(str(r.get("filename", ""))), + "line_number": r.get("line_number"), + "issue_text": r.get("issue_text"), + "more_info": r.get("more_info"), + "code": r.get("code"), + } + ) + findings.sort(key=lambda x: (x["filename"], x["line_number"] or 0, x["test_id"] or "")) + by_sev = Counter(f["severity"] for f in findings) + out: dict[str, Any] = { + "tool": "bandit", + "schema": SCHEMA, + "count": len(findings), + "by_severity": dict(sorted(by_sev.items())), + "findings": findings, + } + if collapsed: + out["duplicates_collapsed"] = collapsed + return out + + +def cmd_normalize(args: argparse.Namespace) -> int: + data = load_json(args.input) + dump_json(normalize_payload(data), args.out) + return 0 + + +def render_report(norm: dict[str, Any], source: str) -> str: + lines: list[str] = [] + lines.append("# Rapport Bandit (lisible)") + lines.append("") + lines.append(f"- **Source** : `{source}`") + lines.append(f"- **Findings** : {norm['count']}") + lines.append(f"- **Par severite** : {norm.get('by_severity') or {}}") + if norm.get("duplicates_collapsed"): + lines.append( + f"- **Doublons fusionnes** : {norm['duplicates_collapsed']}" + ) + lines.append("") + lines.append("| Severite | Confiance | ID | Fichier | Ligne | Message |") + lines.append("|----------|-----------|----|---------|-------|---------|") + for f in norm["findings"]: + msg = (f.get("issue_text") or "").replace("|", "\\|").replace("\n", " ") + if len(msg) > 80: + msg = msg[:77] + "..." + lines.append( + f"| {f.get('severity','')} | {f.get('confidence','')} | `{f.get('test_id','')}` | " + f"`{f.get('filename','')}` | {f.get('line_number','')} | {msg} |" + ) + lines.append("") + highs = [f for f in norm["findings"] if f.get("severity") == "HIGH"] + if highs: + lines.append("## Detail - HIGH") + lines.append("") + for f in highs: + lines.append(f"### `{f.get('test_id')}` - `{f.get('filename')}:{f.get('line_number')}`") + lines.append("") + lines.append(f"{f.get('issue_text') or ''}") + lines.append("") + code = f.get("code") or "" + if code: + lines.append("```") + lines.append(code.rstrip()) + lines.append("```") + lines.append("") + return "\n".join(lines) + + +def cmd_report(args: argparse.Namespace) -> int: + norm = normalize_payload(load_json(args.input)) + write_text(render_report(norm, str(args.input)), args.out) + return 0 + + +def index_by_key(norm: dict[str, Any]) -> dict[str, dict[str, Any]]: + return {f["key"]: f for f in norm["findings"]} + + +def cmd_diff(args: argparse.Namespace) -> int: + a = normalize_payload(load_json(args.baseline)) + b = normalize_payload(load_json(args.current)) + ia, ib = index_by_key(a), index_by_key(b) + keys_a, keys_b = set(ia), set(ib) + new = sorted(keys_b - keys_a) + resolved = sorted(keys_a - keys_b) + stable = sorted(keys_a & keys_b) + + lines: list[str] = [] + lines.append("# Diff Bandit") + lines.append("") + lines.append(f"- **Baseline (A)** : `{args.baseline}` - {a['count']} findings") + lines.append(f"- **Courant (B)** : `{args.current}` - {b['count']} findings") + lines.append(f"- **Nouveaux** : {len(new)}") + lines.append(f"- **Resolus** : {len(resolved)}") + lines.append(f"- **Stables** : {len(stable)}") + lines.append("") + + def table(title: str, keys: list[str], src: dict[str, dict[str, Any]]) -> None: + lines.append(f"## {title} ({len(keys)})") + lines.append("") + if not keys: + lines.append("*(aucun)*") + lines.append("") + return + lines.append("| Severite | ID | Fichier | Ligne | Message |") + lines.append("|----------|----|---------|-------|---------|") + for k in keys: + f = src[k] + msg = (f.get("issue_text") or "").replace("|", "\\|").replace("\n", " ") + if len(msg) > 60: + msg = msg[:57] + "..." + lines.append( + f"| {f.get('severity','')} | `{f.get('test_id','')}` | `{f.get('filename','')}` | " + f"{f.get('line_number','')} | {msg} |" + ) + lines.append("") + + table("Nouveaux (dans B, absents de A)", new, ib) + table("Resolus (dans A, absents de B)", resolved, ia) + + write_text("\n".join(lines), args.out) + + fail = False + if args.fail_on_new and new: + fail = True + if args.fail_on_severity: + thr = sev_rank_bandit(args.fail_on_severity) + for k in new: + if sev_rank_bandit(ib[k].get("severity", "")) >= thr: + fail = True + break + + print( + f"bandit_diff: new={len(new)} resolved={len(resolved)} stable={len(stable)}", + file=sys.stderr, + ) + return 1 if fail else 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + description="Bandit JSON : normalize / report / diff (compatible CI)." + ) + sub = p.add_subparsers(dest="cmd", required=True) + + n = sub.add_parser("normalize", help="JSON stable trie (cle d'identite)") + n.add_argument("input", help="bandit.json brut") + n.add_argument("-o", "--out", default="-", help="sortie (defaut: stdout)") + n.set_defaults(func=cmd_normalize) + + r = sub.add_parser("report", help="Rapport Markdown lisible") + r.add_argument("input", help="bandit.json brut ou normalise") + r.add_argument("-o", "--out", default="-", help="sortie .md (defaut: stdout)") + r.set_defaults(func=cmd_report) + + d = sub.add_parser("diff", help="Comparer deux analyses (A=baseline, B=courant)") + d.add_argument("baseline", help="JSON A (reference)") + d.add_argument("current", help="JSON B (nouveau run)") + d.add_argument("-o", "--out", default="-", help="rapport diff .md") + d.add_argument( + "--fail-on-new", + action="store_true", + help="exit 1 si findings nouveaux dans B", + ) + d.add_argument( + "--fail-on-severity", + metavar="LEVEL", + help="exit 1 si un NOUVEAU finding >= LEVEL (HIGH|MEDIUM|LOW)", + ) + d.set_defaults(func=cmd_diff) + return p + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + try: + return int(args.func(args)) + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/security_audit/baseline/README.md b/security_audit/baseline/README.md new file mode 100644 index 0000000..e06e4f3 --- /dev/null +++ b/security_audit/baseline/README.md @@ -0,0 +1,18 @@ +# Baselines SAST + +Ce dossier contient les fichiers JSON de reference utilises pour comparer +les nouveaux scans Bandit et Semgrep. + +Fichiers attendus : + +- `bandit.json` +- `semgrep.json` + +Principe : + +- le workflow produit les JSON courants dans `security_audit/out/` +- s'il trouve une baseline correspondante ici, il genere un diff +- `--fail-on-new` fait echouer le job si de nouveaux findings apparaissent + +Au premier deploiement, ce dossier peut rester sans baseline : le workflow +fera simplement les scans et publiera les artefacts. diff --git a/security_audit/requirements.txt b/security_audit/requirements.txt new file mode 100644 index 0000000..abeabc1 --- /dev/null +++ b/security_audit/requirements.txt @@ -0,0 +1,3 @@ +bandit +semgrep +pip-audit diff --git a/security_audit/semgrep_json.py b/security_audit/semgrep_json.py new file mode 100644 index 0000000..e48b656 --- /dev/null +++ b/security_audit/semgrep_json.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +""" +semgrep_json.py - rendre lisible / normaliser / comparer des sorties Semgrep JSON. + +Meme pattern que bandit_json.py (inspiration sort/compare nodes.json de l'encadrant). + +Exemples : + ./semgrep_json.py normalize semgrep_security.json -o semgrep.normalized.json + ./semgrep_json.py report semgrep_security.json -o semgrep.md + ./semgrep_json.py diff old.json new.json -o diff.md --fail-on-new +""" + +from __future__ import annotations + +import argparse +import sys +from collections import Counter +from typing import Any + +from _sast_json_common import ( + dump_json, + load_json, + norm_path, + sev_rank_semgrep, + write_text, +) + + +SCHEMA = "tiledviz.sast.semgrep.normalized.v1" + + +def _looks_like_fingerprint(fp: str) -> bool: + """Semgrep OSS peut mettre un libelle (ex. 'requires login') dans fingerprint.""" + s = str(fp).strip() + if len(s) < 16 or " " in s: + return False + return True + + +def finding_key(r: dict[str, Any]) -> str: + if r.get("key") and "start_line" in r: + return str(r["key"]) + extra = r.get("extra") or {} + fp = extra.get("fingerprint") or r.get("fingerprint") + if fp and _looks_like_fingerprint(str(fp)): + return f"fp::{fp}" + path = norm_path(str(r.get("path", ""))) + start = r.get("start") or {} + line = start.get("line") if start else r.get("start_line", "") + return f"{r.get('check_id', '')}::{path}::{line}" + + +def extract_results(data: Any) -> list[dict[str, Any]]: + if isinstance(data, dict) and data.get("schema") == SCHEMA: + return list(data.get("findings") or []) + if isinstance(data, dict) and "results" in data: + return list(data["results"] or []) + if isinstance(data, dict) and "findings" in data: + return list(data["findings"] or []) + if isinstance(data, list): + return list(data) + raise ValueError("JSON Semgrep attendu : objet avec cle 'results'") + + +def short_rule(check_id: str) -> str: + if not check_id: + return "" + return check_id.rsplit(".", 1)[-1] + + +def normalize_payload(data: Any) -> dict[str, Any]: + if isinstance(data, dict) and data.get("schema") == SCHEMA: + return data + + results = extract_results(data) + findings: list[dict[str, Any]] = [] + seen: set[str] = set() + collapsed = 0 + for r in results: + extra = r.get("extra") or {} + start = r.get("start") or {} + end = r.get("end") or {} + key = finding_key(r) + if key in seen: + collapsed += 1 + continue + seen.add(key) + sev = (extra.get("severity") or r.get("severity") or "").upper() + path = norm_path(str(r.get("path", ""))) + start_line = start.get("line") if start else r.get("start_line") + end_line = end.get("line") if end else r.get("end_line") + findings.append( + { + "key": key, + "check_id": r.get("check_id"), + "rule_short": short_rule(str(r.get("check_id") or "")), + "severity": sev, + "path": path, + "start_line": start_line, + "end_line": end_line, + "message": extra.get("message") or r.get("message"), + "fingerprint": extra.get("fingerprint") or r.get("fingerprint"), + "lines": extra.get("lines") or r.get("lines"), + } + ) + findings.sort( + key=lambda x: (x["path"], x["start_line"] or 0, x["check_id"] or "") + ) + by_sev = Counter(f["severity"] for f in findings) + meta: dict[str, Any] = {} + if isinstance(data, dict) and "version" in data: + meta = {"version": data.get("version"), "engine": data.get("engine_requested")} + elif isinstance(data, dict) and data.get("meta"): + meta = dict(data["meta"]) + out: dict[str, Any] = { + "tool": "semgrep", + "schema": SCHEMA, + "meta": meta, + "count": len(findings), + "by_severity": dict(sorted(by_sev.items())), + "findings": findings, + } + if collapsed: + out["duplicates_collapsed"] = collapsed + return out + + +def cmd_normalize(args: argparse.Namespace) -> int: + dump_json(normalize_payload(load_json(args.input)), args.out) + return 0 + + +def render_report(norm: dict[str, Any], source: str) -> str: + lines: list[str] = [] + lines.append("# Rapport Semgrep (lisible)") + lines.append("") + lines.append(f"- **Source** : `{source}`") + if norm.get("meta"): + lines.append(f"- **Meta** : {norm['meta']}") + lines.append(f"- **Findings** : {norm['count']}") + lines.append(f"- **Par severite** : {norm.get('by_severity') or {}}") + if norm.get("duplicates_collapsed"): + lines.append( + f"- **Doublons fusionnes** : {norm['duplicates_collapsed']}" + ) + lines.append("") + lines.append("| Severite | Regle | Fichier | Ligne | Message |") + lines.append("|----------|-------|---------|-------|---------|") + for f in norm["findings"]: + msg = (f.get("message") or "").replace("|", "\\|").replace("\n", " ") + if len(msg) > 80: + msg = msg[:77] + "..." + rule = f.get("rule_short") or f.get("check_id") or "" + lines.append( + f"| {f.get('severity','')} | `{rule}` | `{f.get('path','')}` | " + f"{f.get('start_line','')} | {msg} |" + ) + lines.append("") + errs = [f for f in norm["findings"] if f.get("severity") in ("ERROR", "HIGH")] + if errs: + lines.append("## Detail - ERROR / HIGH") + lines.append("") + for f in errs: + lines.append( + f"### `{f.get('rule_short')}` - `{f.get('path')}:{f.get('start_line')}`" + ) + lines.append("") + lines.append(f"{f.get('message') or ''}") + lines.append("") + snippet = f.get("lines") or "" + if snippet: + lines.append("```") + lines.append(str(snippet).rstrip()) + lines.append("```") + lines.append("") + lines.append(f"- check_id : `{f.get('check_id')}`") + lines.append("") + return "\n".join(lines) + + +def cmd_report(args: argparse.Namespace) -> int: + norm = normalize_payload(load_json(args.input)) + write_text(render_report(norm, str(args.input)), args.out) + return 0 + + +def index_by_key(norm: dict[str, Any]) -> dict[str, dict[str, Any]]: + return {f["key"]: f for f in norm["findings"]} + + +def cmd_diff(args: argparse.Namespace) -> int: + a = normalize_payload(load_json(args.baseline)) + b = normalize_payload(load_json(args.current)) + ia, ib = index_by_key(a), index_by_key(b) + keys_a, keys_b = set(ia), set(ib) + new = sorted(keys_b - keys_a) + resolved = sorted(keys_a - keys_b) + stable = sorted(keys_a & keys_b) + + lines: list[str] = [] + lines.append("# Diff Semgrep") + lines.append("") + lines.append(f"- **Baseline (A)** : `{args.baseline}` - {a['count']} findings") + lines.append(f"- **Courant (B)** : `{args.current}` - {b['count']} findings") + lines.append(f"- **Nouveaux** : {len(new)}") + lines.append(f"- **Resolus** : {len(resolved)}") + lines.append(f"- **Stables** : {len(stable)}") + lines.append("") + + def table(title: str, keys: list[str], src: dict[str, dict[str, Any]]) -> None: + lines.append(f"## {title} ({len(keys)})") + lines.append("") + if not keys: + lines.append("*(aucun)*") + lines.append("") + return + lines.append("| Severite | Regle | Fichier | Ligne | Message |") + lines.append("|----------|-------|---------|-------|---------|") + for k in keys: + f = src[k] + msg = (f.get("message") or "").replace("|", "\\|").replace("\n", " ") + if len(msg) > 60: + msg = msg[:57] + "..." + rule = f.get("rule_short") or "" + lines.append( + f"| {f.get('severity','')} | `{rule}` | `{f.get('path','')}` | " + f"{f.get('start_line','')} | {msg} |" + ) + lines.append("") + + table("Nouveaux (dans B, absents de A)", new, ib) + table("Resolus (dans A, absents de B)", resolved, ia) + + write_text("\n".join(lines), args.out) + + fail = False + if args.fail_on_new and new: + fail = True + if args.fail_on_severity: + thr = sev_rank_semgrep(args.fail_on_severity) + for k in new: + if sev_rank_semgrep(ib[k].get("severity", "")) >= thr: + fail = True + break + + print( + f"semgrep_diff: new={len(new)} resolved={len(resolved)} stable={len(stable)}", + file=sys.stderr, + ) + return 1 if fail else 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + description="Semgrep JSON : normalize / report / diff (compatible CI)." + ) + sub = p.add_subparsers(dest="cmd", required=True) + + n = sub.add_parser("normalize", help="JSON stable trie") + n.add_argument("input") + n.add_argument("-o", "--out", default="-") + n.set_defaults(func=cmd_normalize) + + r = sub.add_parser("report", help="Rapport Markdown lisible") + r.add_argument("input") + r.add_argument("-o", "--out", default="-") + r.set_defaults(func=cmd_report) + + d = sub.add_parser("diff", help="Comparer deux analyses") + d.add_argument("baseline") + d.add_argument("current") + d.add_argument("-o", "--out", default="-") + d.add_argument("--fail-on-new", action="store_true") + d.add_argument( + "--fail-on-severity", + metavar="LEVEL", + help="seuil sur NOUVEAUX (ERROR|WARNING|INFO)", + ) + d.set_defaults(func=cmd_diff) + return p + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + try: + return int(args.func(args)) + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/security_audit/test_sast_json.py b/security_audit/test_sast_json.py new file mode 100644 index 0000000..e7bf0ec --- /dev/null +++ b/security_audit/test_sast_json.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Tests unitaires - bandit_json.py / semgrep_json.py + +Usage (depuis ce dossier) : + python3 test_sast_json.py + python3 -m unittest test_sast_json -v +""" + +from __future__ import annotations + +import json +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPTS = Path(__file__).resolve().parent +PREUVES_SAST = SCRIPTS.parent.parent / "06_preuves" / "figement_toe" / "sast" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import bandit_json as bj +import semgrep_json as sj + + +BANDIT_RAW = { + "results": [ + { + "test_id": "B605", + "test_name": "start_process_with_a_shell", + "issue_severity": "HIGH", + "issue_confidence": "HIGH", + "filename": "./TVConnections/TVConnection.py", + "line_number": 63, + "issue_text": "Starting a process with a shell", + "code": "os.system(cmd)", + }, + { + "test_id": "B307", + "test_name": "blacklist", + "issue_severity": "MEDIUM", + "issue_confidence": "HIGH", + "filename": "TVConnections/TVConnection.py", + "line_number": 183, + "issue_text": "Use of possibly insecure function", + "code": "eval(x)", + }, + { + "test_id": "B605", + "test_name": "start_process_with_a_shell", + "issue_severity": "HIGH", + "issue_confidence": "HIGH", + "filename": "TVConnections/TVConnection.py", + "line_number": 63, + "issue_text": "Starting a process with a shell (dup)", + "code": "os.system(cmd)", + }, + ] +} + +SEMGREP_RAW = { + "version": "1.0.0", + "engine_requested": "OSS", + "results": [ + { + "check_id": "python.lang.security.audit.subprocess-shell-true.subprocess-shell-true", + "path": "TVConnections/TileServer.py", + "start": {"line": 488}, + "end": {"line": 488}, + "extra": { + "severity": "ERROR", + "message": "Found subprocess with shell=True", + "fingerprint": "requires login", + "lines": "Popen(cmd, shell=True)", + }, + }, + { + "check_id": "python.lang.security.audit.avoid-bind-to-all-interfaces.avoid-bind-to-all-interfaces", + "path": "./TVConnections/TileServer.py", + "start": {"line": 131}, + "end": {"line": 131}, + "extra": { + "severity": "INFO", + "message": "bind to 0.0.0.0", + "fingerprint": "requires login", + "lines": "sock.bind(('0.0.0.0', 80))", + }, + }, + ], +} + + +def _write_json(path: Path, data: object) -> Path: + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return path + + +class TestBanditJson(unittest.TestCase): + def test_normalize_collapses_duplicates_and_paths(self) -> None: + norm = bj.normalize_payload(BANDIT_RAW) + self.assertEqual(norm["schema"], bj.SCHEMA) + self.assertEqual(norm["count"], 2) + self.assertEqual(norm.get("duplicates_collapsed"), 1) + keys = {f["key"] for f in norm["findings"]} + self.assertIn("B605::TVConnections/TVConnection.py::63", keys) + self.assertTrue(all(not f["filename"].startswith("./") for f in norm["findings"])) + + def test_normalize_idempotent(self) -> None: + n1 = bj.normalize_payload(BANDIT_RAW) + n2 = bj.normalize_payload(n1) + self.assertEqual(n1, n2) + self.assertEqual(n2["findings"][0]["severity"], "HIGH") + + def test_diff_same_exit_0(self) -> None: + with tempfile.TemporaryDirectory() as td: + t = Path(td) + a = _write_json(t / "a.json", BANDIT_RAW) + out = t / "diff.md" + code = bj.main(["diff", str(a), str(a), "-o", str(out), "--fail-on-new"]) + self.assertEqual(code, 0) + text = out.read_text(encoding="utf-8") + self.assertIn("**Nouveaux** : 0", text) + + def test_diff_new_finding_fail_on_new(self) -> None: + baseline = bj.normalize_payload(BANDIT_RAW) + current = bj.normalize_payload(BANDIT_RAW) + extra = dict(current["findings"][0]) + extra["line_number"] = 999 + extra["key"] = f"{extra['test_id']}::{extra['filename']}::999" + extra["issue_text"] = "FAKE NEW" + current["findings"] = list(current["findings"]) + [extra] + current["count"] = len(current["findings"]) + + with tempfile.TemporaryDirectory() as td: + t = Path(td) + a = _write_json(t / "a.json", baseline) + b = _write_json(t / "b.json", current) + out = t / "diff.md" + code = bj.main(["diff", str(a), str(b), "-o", str(out), "--fail-on-new"]) + self.assertEqual(code, 1) + self.assertIn("FAKE NEW", out.read_text(encoding="utf-8")) + + def test_report_markdown(self) -> None: + with tempfile.TemporaryDirectory() as td: + t = Path(td) + src = _write_json(t / "bandit.json", BANDIT_RAW) + out = t / "report.md" + code = bj.main(["report", str(src), "-o", str(out)]) + self.assertEqual(code, 0) + text = out.read_text(encoding="utf-8") + self.assertIn("# Rapport Bandit", text) + self.assertIn("B605", text) + self.assertIn("Doublons fusionnes", text) + + +class TestSemgrepJson(unittest.TestCase): + def test_normalize_ignores_fake_fingerprint(self) -> None: + norm = sj.normalize_payload(SEMGREP_RAW) + self.assertEqual(norm["count"], 2) + for f in norm["findings"]: + self.assertFalse(f["key"].startswith("fp::"), f["key"]) + self.assertIn("::", f["key"]) + + def test_normalize_idempotent(self) -> None: + n1 = sj.normalize_payload(SEMGREP_RAW) + n2 = sj.normalize_payload(n1) + self.assertEqual(n1["count"], n2["count"]) + self.assertEqual( + [f["key"] for f in n1["findings"]], + [f["key"] for f in n2["findings"]], + ) + sevs = {f["severity"] for f in n2["findings"]} + self.assertEqual(sevs, {"ERROR", "INFO"}) + + def test_diff_new_finding_fail_on_new(self) -> None: + baseline = sj.normalize_payload(SEMGREP_RAW) + current = sj.normalize_payload(SEMGREP_RAW) + extra = dict(current["findings"][0]) + extra["start_line"] = 999 + extra["key"] = f"{extra['check_id']}::{extra['path']}::999" + extra["message"] = "FAKE NEW SEMGREP" + current["findings"] = list(current["findings"]) + [extra] + current["count"] = len(current["findings"]) + + with tempfile.TemporaryDirectory() as td: + t = Path(td) + a = _write_json(t / "a.json", baseline) + b = _write_json(t / "b.json", current) + out = t / "diff.md" + code = sj.main(["diff", str(a), str(b), "-o", str(out), "--fail-on-new"]) + self.assertEqual(code, 1) + self.assertIn("FAKE NEW SEMGREP", out.read_text(encoding="utf-8")) + + def test_report_markdown(self) -> None: + with tempfile.TemporaryDirectory() as td: + t = Path(td) + src = _write_json(t / "semgrep.json", SEMGREP_RAW) + out = t / "report.md" + code = sj.main(["report", str(src), "-o", str(out)]) + self.assertEqual(code, 0) + text = out.read_text(encoding="utf-8") + self.assertIn("# Rapport Semgrep", text) + self.assertIn("subprocess-shell-true", text) + + +@unittest.skipUnless( + (PREUVES_SAST / "bandit.json").is_file() + and (PREUVES_SAST / "semgrep_security.json").is_file(), + "preuves figement_toe absentes", +) +class TestAgainstLabEvidence(unittest.TestCase): + def test_bandit_self_diff(self) -> None: + path = PREUVES_SAST / "bandit.json" + with tempfile.TemporaryDirectory() as td: + out = Path(td) / "d.md" + code = bj.main(["diff", str(path), str(path), "-o", str(out), "--fail-on-new"]) + self.assertEqual(code, 0) + + def test_semgrep_self_diff(self) -> None: + path = PREUVES_SAST / "semgrep_security.json" + with tempfile.TemporaryDirectory() as td: + out = Path(td) / "d.md" + code = sj.main(["diff", str(path), str(path), "-o", str(out), "--fail-on-new"]) + self.assertEqual(code, 0) + + +if __name__ == "__main__": + unittest.main() From f543298693ea45b07e96126f98b4f1e8d604248b Mon Sep 17 00:00:00 2001 From: ARISAWSS Date: Tue, 28 Jul 2026 09:44:05 +0200 Subject: [PATCH 2/3] Ignore generated Python bytecode. Keep the security audit tooling branch clean by removing test-generated cache files and preventing them from being recommitted. Co-authored-by: Cursor --- security_audit/.gitignore | 2 ++ .../_sast_json_common.cpython-312.pyc | Bin 2879 -> 0 bytes .../__pycache__/bandit_json.cpython-312.pyc | Bin 14717 -> 0 bytes .../__pycache__/semgrep_json.cpython-312.pyc | Bin 16434 -> 0 bytes .../__pycache__/test_sast_json.cpython-312.pyc | Bin 13236 -> 0 bytes 5 files changed, 2 insertions(+) create mode 100644 security_audit/.gitignore delete mode 100644 security_audit/__pycache__/_sast_json_common.cpython-312.pyc delete mode 100644 security_audit/__pycache__/bandit_json.cpython-312.pyc delete mode 100644 security_audit/__pycache__/semgrep_json.cpython-312.pyc delete mode 100644 security_audit/__pycache__/test_sast_json.cpython-312.pyc diff --git a/security_audit/.gitignore b/security_audit/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/security_audit/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/security_audit/__pycache__/_sast_json_common.cpython-312.pyc b/security_audit/__pycache__/_sast_json_common.cpython-312.pyc deleted file mode 100644 index 84431b038d11bb71bd5fd90c44bceba9a1a3607d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2879 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 diff --git a/security_audit/__pycache__/semgrep_json.cpython-312.pyc b/security_audit/__pycache__/semgrep_json.cpython-312.pyc deleted file mode 100644 index 47cfec09a626af7cdeeb2475c31b567b900408ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16434 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 U!;2SgUi>{(4&IMc4Q}lJ1x(Ok@&Et; diff --git a/security_audit/__pycache__/test_sast_json.cpython-312.pyc b/security_audit/__pycache__/test_sast_json.cpython-312.pyc deleted file mode 100644 index f40e4950c1d740eb40327439c770d9348b7a246e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13236 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 Date: Tue, 28 Jul 2026 10:00:28 +0200 Subject: [PATCH 3/3] Harden empty SAST output handling. Keep report generation stable by writing empty JSON payloads when Bandit or Semgrep do not emit an output file. Co-authored-by: Cursor --- .github/workflows/security_audit.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/security_audit.yml b/.github/workflows/security_audit.yml index 2422bfd..77c3e79 100644 --- a/.github/workflows/security_audit.yml +++ b/.github/workflows/security_audit.yml @@ -34,6 +34,9 @@ jobs: bandit -r TVWeb/app TVSecure TVConnections \ -f json \ -o security_audit/out/bandit.json || true + if [ ! -s security_audit/out/bandit.json ]; then + printf '{\n "results": []\n}\n' > security_audit/out/bandit.json + fi - name: Run Semgrep run: | @@ -43,6 +46,9 @@ jobs: --json \ --output security_audit/out/semgrep.json \ TVWeb/app TVSecure TVConnections || true + if [ ! -s security_audit/out/semgrep.json ]; then + printf '{\n "results": []\n}\n' > security_audit/out/semgrep.json + fi - name: Render readable reports run: |