├── README.MD └── src ├── File-Tail-Multi-0.2.tar.gz ├── blcheck.pl ├── blcheck.sh ├── build_mail_ipset.pl ├── build_zimbra.sh ├── check_attacks.pl ├── check_dumpster.sh ├── check_login.pl ├── check_recipients.pl ├── check_rejected_spam.pl ├── check_sa.sh ├── compare_next_zimbra_tag.sh ├── compare_pimbra.sh ├── compare_zimbra_updates.sh ├── dump_trained_spam.sh ├── inspect_mail.pl ├── ip_manager.pl ├── move-file-repository.sh ├── show-me-the-branch.sh ├── show-mobile.sh ├── show-user-counts-mysql.sh ├── switch_mail.sh ├── tag-versions.sh ├── vi-email.sh ├── zm-audit-log.pl ├── zmAveSizeMsg.sh ├── zmAveSizeMsgStoredProcedure.sh ├── zmShowUserCounts.sh ├── zmbounceMsg ├── zmcertNotice.sh ├── zmcertNotice2.sh ├── zmcopyTrain └── zmdu.sh /README.MD: -------------------------------------------------------------------------------- 1 | # Some examples of scripts in src 2 | 3 | # check_login.pl 4 | 5 | # Purpose 6 | Report various accesses to the zimbra mail system (web,imap,pop,etc). List the ip's per user and the number of access per ip. Need permission to /opt/zimbra/audit.log as the user running this script. 7 | 8 | # Operation 9 | ```bash 10 | % check_login.pl 11 | ``` 12 | 13 | # Usage: 14 | ~~~~ 15 | usage: % check_login.pl 16 | [--color=] 17 | [--srchuser=] 18 | [--fail=] 19 | [--gethost=] 20 | [--help] 21 | where: 22 | --color|c: color to be used for FAILED login message information 23 | --srchuser|s: print ONLY the logins/failed logins for 24 | --fail|f: if 'user': print ONLY users who have failed logins. If 'ip': print ONLY the failed login attempts. 'none': print all records regardless if failure 25 | --gethost|g: values of 'all', 'fail' or 'none'. Perform a GETHOSTBYADDR for all IPs, only on FAILED login attempts, or don't perform this action (none) 26 | --help|h: this message 27 | "; 28 | example: % check_login.pl -f=user #only the accounts with failed logins 29 | % check_login.pl -f ip #only the accounts and the ip that failed 30 | % check_login.pl -fail=ip # same as above 31 | % check_login.pl --fail ip # same as above 32 | % check_login.pl -f ip -h #list only ip's that failed for accounts resolve ip to domain name 33 | % check_login.pl -g fail #list only accounts that had a failed login 34 | % check_login.pl -g all #list all accounts and resolve ip to domain name 35 | % check_login.pl -c RED -f ip #change color and list only failed ip's 36 | % check_login.pl -s user -f ip -g fail #list all failed ip addresses with ip to domain name 37 | % check_login.pl -s user.com -f ip -g fail #list all failed ip addresses with ip to domain name 38 | ~~~~ 39 | 40 | ```bash 41 | # Sample Report (original before recent update) 42 | annaSmith@example.com 43 | [ 2] - 24.118.12.16 44 | 45 | flo@example.net 46 | [ 3] - 24.118.12.16 47 | 48 | annaSmith@example.net 49 | [ 15] - 24.118.12.16 50 | 51 | bldd1@example.com 52 | [ 1] - 220.180.172.173 53 | [ 1] - 58.248.164.150 54 | [ 1] - 41.210.223.10 55 | [ 1] - 222.242.229.42 56 | [ 1] - 222.191.233.238 57 | [ 1] - 80.13.84.146 58 | [ 1] - 213.138.74.85 59 | [ 1] - 220.170.196.198 60 | [ 1] - 117.158.101.182 61 | [ 1] - 59.61.79.82 62 | [ 1] - 218.64.77.6 63 | [ 1] - 59.61.79.82 failed imap 64 | [ 1] - 218.64.77.6 failed imap 65 | [ 1] - 213.138.74.85 failed imap 66 | [ 1] - 117.158.101.182 failed imap 67 | [ 1] - 41.210.223.10 failed imap 68 | [ 1] - 220.170.196.198 failed imap 69 | [ 1] - 222.191.233.238 failed imap 70 | [ 1] - 80.13.84.146 failed imap 71 | [ 1] - 222.242.229.42 failed imap 72 | [ 1] - 220.180.172.173 failed imap 73 | [ 1] - 58.248.164.150 failed imap 74 | 75 | ``` 76 | # CAVEATS 77 | The failed report happens after the ip access report per user in the same list. A count of 1 for example for a failed doesn't mean they had a successful login. In other words, if you see a fail with an ip address and it has a count of 1 but the ip address has a count of 2 above. That means one time was successful and the other time was failed. 78 | 79 | # Other Scripts 80 | 81 | # build_zimbra.sh 82 | Automated script to build Zimbra FOSS for version 8.8.15, 9.0.0, and 10.0.0 83 | 84 | # zmcopyTrain 85 | Copies ham/spam to /tmp to view how uses are training spam 86 | 87 | # zmbounceMsg 88 | Shows commands necessary to bounce messages to user that was stopped by virus dection. 89 | Also show where the physical files are to allow admin to view the contents. 90 | 91 | # check_rejected_spam.pl 92 | Start of a more general purpose summary script 93 | 94 | # check_recipients.pl 95 | Tracking of outgoing messages based on how many they are sending. 96 | Can we determine a hacked account by the type of outgoing? 97 | 98 | # check_attacks.pl 99 | Track attacks against zimbra installations. Can output list of ip's or ipsets for blocking. Searches all /opt/zimbra/log/nginx.access\* logs. 100 | It does this but orders the output by counts and pretty prints the output. Documented in this thread. https://forums.zimbra.org/viewtopic.php?f=15&t=66092 101 | 102 | ```bash 103 | % zcat -f /opt/zimbra/log/nginx.ac* |grep Autodiscover 104 | 95.179.215.180:36914 - - [29/Apr/2019:06:04:23 -0700] "POST /Autodiscover/Autodiscover.xml HTTP/1.1" 400 567 "-" "python-requests/2.18.4" "X.X.X.X:8080" 105 | 159.69.81.117:46880 - - [28/Apr/2019:05:19:00 -0700] "POST /Autodiscover/Autodiscover.xml HTTP/1.1" 400 567 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, lik 106 | e Gecko) Chrome/24.0.1309.0 Safari/537.17" "X.X.X.X:8080" 107 | ``` 108 | It attempts to filter out local users. The first step is to customize this for your zimbra installation the first time you run it. 109 | Search for STEP 1 in the code. By default, it returns attackers which are everyone else but the local users. Local users can also be included by the --usertype=all option or just the local users by --usertype=local 110 | 111 | # Sample outputs 112 | ```bash 113 | % check_attacks.pl 114 | 115 | ------------------------------------------------------------------------------------------------------------ 116 | [ 400] \x03\x00\x00*%\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Test bot 117 | [ 400] \x03\x00\x00*%\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Test bot 118 | Attacker from 185.153.198.201 2 Requests - Score 100% 119 | ------------------------------------------------------------------------------------------------------------ 120 | [ 200] GET / Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1 121 | [ 404] GET /HNAP1/ Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1 122 | Attacker from 185.190.149.69 2 Requests - Score 25% 123 | ------------------------------------------------------------------------------------------------------------ 124 | [ 400] \x03\x00\x00/*\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Administr bot 125 | Attacker from 185.209.0.12 1 Requests - Score 100% 126 | ------------------------------------------------------------------------------------------------------------ 127 | [ 200] GET / Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0) 128 | Attacker from 185.222.209.87 1 Requests - Score 25% 129 | ------------------------------------------------------------------------------------------------------------ 130 | [ 404] GET /.env Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36 131 | Attacker from 185.234.218.18 1 Requests - Score 25% 132 | ------------------------------------------------------------------------------------------------------------ 133 | [ 400] \x03\x00\x00*%\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Test bot 134 | [ 400] \x03\x00\x00*%\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Test bot 135 | Attacker from 193.188.22.127 2 Requests - Score 100% 136 | ------------------------------------------------------------------------------------------------------------ 137 | [ 200] GET / Mozilla/5.0 zgrab/0.x 138 | Attacker from 198.108.67.16 1 Requests - Score 25% 139 | ------------------------------------------------------------------------------------------------------------ 140 | [ 400] POST /Autodiscover/Autodiscover.xml python-requests/2.18.4 141 | Attacker from 95.179.215.180 1 Requests - Score 25% 142 | ``` 143 | Generate list of ip's. 144 | ```bash 145 | % check_attacks.pl --IPlist | head -5 146 | 103.237.145.12 147 | 106.12.89.13 148 | 107.170.204.68 149 | 107.170.240.102 150 | 107.170.251.213 151 | ``` 152 | Generate list of ip's that had a status code of 400 153 | ```bash 154 | % check_attacks.pl --IPlist --pstatus=400 | head -5 155 | 138.246.253.5 156 | 164.52.24.162 157 | 185.153.198.201 158 | 185.209.0.12 159 | 193.188.22.127 160 | 209.250.252.220 161 | 45.227.255.99 162 | 46.161.27.112 163 | 5.188.210.101 164 | 51.38.12.13 165 | 54.187.17.116 166 | 59.36.132.222 167 | 61.219.11.153 168 | 66.240.205.34 169 | ``` 170 | Generate list of ip's in ipset format. WARNING. There could be false positives unless you have tuned this program to your users. 171 | ```bash 172 | % check_attacks.pl --IPlist --ipset --pstatus=400 |head -5 173 | ipset add blacklist24hr 138.246.253.5 -exists 174 | ipset add blacklist24hr 164.52.24.162 -exists 175 | ipset add blacklist24hr 185.153.198.201 -exists 176 | ipset add blacklist24hr 185.209.0.12 -exists 177 | ipset add blacklist24hr 193.188.22.127 -exists 178 | ``` 179 | Search by ip addresses 180 | ```bash 181 | % check_attacks.pl --srcip='103.237.145.12|106.12.89.13|107.170.204.68' 182 | [ 404] GET /admin//config.php curl/7.29.0 183 | Attacker from 103.237.145.12 1 Requests - Score 25% 184 | ------------------------------------------------------------------------------------------------------------ 185 | [ 200] GET / Mozilla/5.0 zgrab/0.x 186 | Attacker from 106.12.89.13 1 Requests - Score 25% 187 | ------------------------------------------------------------------------------------------------------------ 188 | [ 200] GET / Mozilla/5.0 zgrab/0.x 189 | Attacker from 107.170.204.68 1 Requests - Score 25% 190 | ``` 191 | Search by ip range to see how many in block are attacking 192 | ```bash 193 | % check_attacks.pl --src 93.119.227 194 | [ 200] GET / Wget/1.13.4 (linux-gnu) 195 | Attacker from 93.119.227.19 1 Requests - Score 100% 196 | ------------------------------------------------------------------------------------------------------------ 197 | [ 200] GET / Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1 198 | Attacker from 93.119.227.34 1 Requests - Score 25% 199 | ------------------------------------------------------------------------------------------------------------ 200 | [ 200] GET / Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0 201 | Attacker from 93.119.227.91 1 Requests - Score 25% 202 | ------------------------------------------------------------------------------------------------------------ 203 | 204 | ``` 205 | Show status 206 | ```bash 207 | % check_attacks.pl --statuscnt 208 | Codes 200 Total: 22680 209 | Codes 206 Total: 1 210 | Codes 301 Total: 16 211 | Codes 302 Total: 17 212 | Codes 304 Total: 9 213 | Codes 400 Total: 23 214 | Codes 403 Total: 5 215 | Codes 404 Total: 8 216 | Codes 499 Total: 156 217 | Codes 500 Total: 12 218 | Codes 503 Total: 4 219 | ``` 220 | Print by code - all 500-509 records. Note: may need to specify --userType=all as it defaults to attackers only 221 | ```bash 222 | ./check_attacks.pl --pstatus=206 --userType=all 223 | [ 206] GET /zimbra/public/sounds/im/alert.wav Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0 224 | Zimbra User from 124.18.10.247 14528 Requests - Score 0% 225 | ------------------------------------------------------------------------------------------------------------ 226 | ``` 227 | List the ip's that your local users access the server from. 228 | ```bash 229 | % check_attacks.pl --iplist --localuser | wc -l 230 | 55 231 | % check_attacks.pl --iplist | wc -l 232 | 88 233 | ``` 234 | Show attackers and then swap out the user agent for time of the attack 235 | ```bash 236 | % check_attacks.pl --srcip '95.179.215.180|18.18.248.17|112.118.155.15|159.69.81.117|212.51.217.211|112.118.155.15' 237 | [ 400] POST /Autodiscover/Autodiscover.xml python-requests/2.21.0 238 | Attacker from 112.118.155.15 1 Requests - Score 100% 239 | ------------------------------------------------------------------------------------------------------------ 240 | [ 200] GET / python-requests/2.21.0 241 | [ 400] POST /Autodiscover/Autodiscover.xml Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17 242 | Attacker from 159.69.81.117 2 Requests - Score 100% 243 | ------------------------------------------------------------------------------------------------------------ 244 | [ 400] POST /Autodiscover/Autodiscover.xml python-requests/2.21.0 245 | [ 400] GET /res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz?v=091214175450&skin=../../../../../../../../../opt/zimbra/conf/localconfig.xml%00 python-requests/2.21.0 246 | Attacker from 18.18.248.17 2 Requests - Score 100% 247 | ------------------------------------------------------------------------------------------------------------ 248 | [ 400] POST /Autodiscover/Autodiscover.xml python-requests/2.18.4 249 | Attacker from 95.179.215.180 1 Requests - Score 100% 250 | ------------------------------------------------------------------------------------------------------------ 251 | ``` 252 | Now swap out user agent field for date field To see when attacks happened. 253 | 254 | ```bash 255 | % check_attacks.pl --srcip '95.179.215.180|18.18.248.17|112.118.155.15|159.69.81.117|212.51.217.211|112.118.155.15' --display=date 256 | [ 400] POST /Autodiscover/Autodiscover.xml 29/Apr/2019:16:39:04 257 | Attacker from 112.118.155.15 1 Requests - Score 100% 258 | ------------------------------------------------------------------------------------------------------------ 259 | [ 200] GET / 28/Apr/2019:05:19:00 260 | [ 400] POST /Autodiscover/Autodiscover.xml 28/Apr/2019:05:19:00 261 | Attacker from 159.69.81.117 2 Requests - Score 100% 262 | ------------------------------------------------------------------------------------------------------------ 263 | [ 400] POST /Autodiscover/Autodiscover.xml 29/Apr/2019:08:08:20 264 | [ 400] GET /res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz?v=091214175450&skin=../../../../../../../../../opt/zimbra/conf/localconfig.xml%00 29/Apr/2019:08:08:22 265 | Attacker from 18.18.248.17 2 Requests - Score 100% 266 | ------------------------------------------------------------------------------------------------------------ 267 | [ 400] POST /Autodiscover/Autodiscover.xml 29/Apr/2019:06:04:23 268 | Attacker from 95.179.215.180 1 Requests - Score 100% 269 | ------------------------------------------------------------------------------------------------------------ 270 | ``` 271 | Search is different in that it locates a match and then prints all requests for that ip. It searches the requests, user agent, date, and referrer all at the same time. You may need to use the --display to confirm those matches. For example, to search by date and if there was a POST, the following search would provide all requests for an ip address if any of those searches were previously found. 272 | ```bash 273 | % check_attacks.pl --display=date --search '30/Apr|Post' 274 | [ 400] POST /autodiscover 28/Apr/2019:10:23:12 275 | Attacker from 47.75.173.76 1 Requests - Score 25% 276 | ------------------------------------------------------------------------------------------------------------ 277 | [ 200] GET / 30/Apr/2019:22:48:54 278 | Attacker from 54.37.16.241 1 Requests - Score 25% 279 | ------------------------------------------------------------------------------------------------------------ 280 | [ 404] GET /admin//config.php 02/May/2019:07:00:52 281 | [ 404] GET /admin//config.php 27/Apr/2019:14:13:19 282 | [ 404] GET /admin//config.php 30/Apr/2019:04:47:15 283 | [ 404] GET /admin//config.php 01/May/2019:05:29:00 284 | Attacker from 138.185.144.75 4 Requests - Score 100% 285 | ``` 286 | Running the same search without the --display gives the same results but uagent is displayed now. 287 | ```bash 288 | % check_attacks.pl --search '30/Apr|Post' 289 | [ 400] POST /autodiscover Mozilla/5.0 (Linux; U; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 290 | Attacker from 47.75.173.76 1 Requests - Score 25% 291 | ------------------------------------------------------------------------------------------------------------ 292 | [ 200] GET / Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0) 293 | Attacker from 54.37.16.241 1 Requests - Score 25% 294 | ------------------------------------------------------------------------------------------------------------ 295 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 296 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 297 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 298 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 299 | Attacker from 138.185.144.75 4 Requests - Score 100% 300 | 301 | % check_attacks.pl --search '\.jsp|\.php' 302 | [ 404] GET /nx8j78af1b.jsp Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 303 | Attacker from 128.14.209.154 1 Requests - Score 100% 304 | ------------------------------------------------------------------------------------------------------------ 305 | [ 404] GET /nx8j78af1b.jsp Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 306 | Attacker from 128.14.209.226 1 Requests - Score 100% 307 | ------------------------------------------------------------------------------------------------------------ 308 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 309 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 310 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 311 | [ 404] GET /admin//config.php curl/7.15.5 (x86_64-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5 312 | Attacker from 138.185.144.75 4 Requests - Score 100% 313 | ------------------------------------------------------------------------------------------------------------ 314 | [ 404] GET /admin/config.php curl/7.29.0 315 | Attacker from 203.147.24.220 1 Requests - Score 100% 316 | ------------------------------------------------------------------------------------------------------------ 317 | [ 404] GET /admin//config.php curl/7.19.7 (x86_64-koji-linux-gnu) libcurl/7.19.7 NSS/3.15.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 318 | Attacker from 221.212.99.106 1 Requests - Score 100% 319 | ------------------------------------------------------------------------------------------------------------ 320 | [ 400] \x05\x01\x00 bot 321 | [ 400] \x04\x01\x00P\x05\xBC\xD2e\x00 bot 322 | [ 400] GET http://5.188.210.101/echo.php Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36 323 | Attacker from 5.188.210.101 3 Requests - Score 100% 324 | ------------------------------------------------------------------------------------------------------------ 325 | ``` 326 | Examples and Help 327 | ```bash 328 | usage: % check_attacker.pl 329 | [--fcolor=] 330 | [--srcip=] 331 | [--localUser ] 332 | [--IPlist ] 333 | [--statuscnt] 334 | [--display="date|upstream|bytes|port|referrer] 335 | [--usertype= 336 | [--pstatus= 337 | [--help] 338 | [--version] 339 | where: 340 | --srcip|sr: print only records matching ip addresses 341 | --statuscnt: prints out the count for each status return code found 342 | --help|h: this message 343 | examples: (-- or - or first few characters of option so not ambigous) 344 | % check_attacker.pl -srcip 10.10.10.1 #only this ip address 345 | % check_attacker.pl -srcip '10.10.10.1|20.20.20.2' #only these ip addresses 346 | % check_attacker.pl -statuscnt #print status codes 347 | % check_attacker.pl --statuscnt #print status codes #same 348 | % check_attacker.pl --localUser #include local users accounts 349 | % check_attacker.pl --IPlist # print list of ips 350 | % check_attacker.pl --IPlist --ipset # print list of ips in ipset format 351 | % check_attacker.pl --IPlist -pstatus='40.' --ipset # print list of ips in ipset format with status code 400..409 352 | % check_attacker.pl --localUser --IPlist # print list of local ips used by local users 353 | % check_attacker.pl --IPlist --ipset | sh # install ip's into ipset 354 | % check_attacker.pl --initIPset # show how to create ipset 355 | % check_attacker.pl -fc RED #change color 356 | % check_attacker.pl --usertype=local # print out strings of only local users 357 | % check_attacker.pl --pstatus='4..' # print out only those requests with a code of 4XX (ie 403, 404, 499) 358 | % check_attacker.pl --usertype=all --pstatus='403|500' # print out only those requests with a code of 403 or 500 for all types (local & attacker) 359 | % check_attacker.pl --display=date # default is to display the user agent 360 | % check_attacker.pl --display=referrer # default is to display the user agent 361 | ``` 362 | -------------------------------------------------------------------------------- /src/File-Tail-Multi-0.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JimDunphy/ZimbraScripts/a1a3ca3dc9a69ccb21bf22e311a0d0144d6133b9/src/File-Tail-Multi-0.2.tar.gz -------------------------------------------------------------------------------- /src/blcheck.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # 4 | # Very Fast: reverse lookup (ASYNC lookups) 5 | # < 1 sec for 129 look ups 6 | # 7 | # usage: blcheck.pl X.X.X.X Y.Y.Y.Y 8 | # cat list | blcheck.pl 9 | # check_attacker.pl --pstatus=400 --iplist | blcheck.pl 10 | # 11 | # Proof of concept. 12 | # 13 | # Author: Jim Dunphy jad AT aesir.com (5/1/2019) 14 | # 15 | # CAVEAT: requires separate installation of Net::DNS::Native and AnyEvent via cpan. 16 | # cpan> install /OLEG/Net-DNS-Native-0.20.tar.gz/ 17 | # cpan> install /MLEHMANN/AnyEvent-7.15.tar.gz/ 18 | # 19 | 20 | use Net::DNS::Native; 21 | use AnyEvent; 22 | use Socket; 23 | #use Data::Dumper qw(Dumper); 24 | 25 | #register at http://www.projecthoneypot.org/httpbl_api.php 26 | # to obtain an API-key (free) 27 | $key="DEFINE KEY"; 28 | 29 | 30 | # from command line or via STDIN 31 | if (@ARGV) { 32 | @ips = @ARGV; 33 | } 34 | else 35 | { 36 | # do some cleanup just in case 37 | while(my $ip = ){ 38 | chomp $ip; 39 | next if ($ip !~ qr/^(?!(\.))(\.?(\d{1,3})(?(?{$^N > 255})(*FAIL))){4}$/); 40 | push(@ips, $ip); 41 | } 42 | } 43 | 44 | #print Dumper \@ips; 45 | 46 | my $dns = Net::DNS::Native->new; 47 | my $cv = AnyEvent->condvar; 48 | $cv->begin; 49 | 50 | #for my $host ('217.182.143.93', '61.219.11.153', '134.119.189.29') 51 | for my $host (@ips) 52 | { 53 | my $target_IP = join('.', reverse split(/\./, $host)).".cbl.abuseat.org"; 54 | #my $target_IP = "$key.".join('.', reverse split(/\./, $host)).".dnsbl.httpbl.org"; 55 | #print "$target_IP\n"; 56 | 57 | my $fh = $dns->inet_aton($target_IP); 58 | $cv->begin; 59 | 60 | my $w; $w = AnyEvent->io( 61 | fh => $fh, 62 | poll => 'r', 63 | cb => sub { 64 | my $ip = $dns->get_result($fh); 65 | print "$host ".inet_ntoa($ip),"\n" if ($ip); 66 | 67 | $cv->end; 68 | undef $w; 69 | } 70 | ) 71 | } 72 | 73 | $cv->end; 74 | $cv->recv; 75 | -------------------------------------------------------------------------------- /src/blcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # usage: blcheck 1.1.1.2 5 | # check_attacker.pl --pstatus=400 --iplist | blcheck.sh 6 | # cat list-of-ips | blcheck.sh 7 | # 8 | # Proof of Concept: what lists to try? 9 | # 10 | # Author: Jim Dunphy jad AT aesir.com (4/29/2019) 11 | # 12 | 13 | BLISTS=" 14 | cbl.abuseat.org 15 | dnsbl.sorbs.net 16 | bl.spamcop.net 17 | zen.spamhaus.org 18 | " 19 | 20 | # register at http://www.projecthoneypot.org/httpbl_api.php 21 | # to obtain an API-key (free) 22 | # 23 | # add this to the BLISTS above - dnsbl.httpbl.org 24 | #HTTPbl_API_KEY="[your_api_key]" 25 | 26 | function lookupIP { 27 | 28 | ip=$1 29 | 30 | if [ -z "${ip}" ];then return;fi 31 | 32 | #assumes proper ip address 33 | reverse=$(echo $ip | awk -F\. '{printf "%s.%s.%s.%s",$4,$3,$2,$1}') 34 | 35 | # -- cycle through all the blacklists 36 | for BL in ${BLISTS} 37 | do 38 | # dig to lookup the name in the blacklist 39 | printf "%-50s" " ${reverse}.${BL}." 40 | if [ "$BL" == "dnsbl.httpbl.org" ]; 41 | then 42 | HIT="$(dig +short -t a ${HTTPbl_API_KEY}.${reverse}.${BL}.)" 43 | echo ${HIT:----} 44 | else 45 | #echo dig +short -t a ${reverse}.${BL}. 46 | HIT="$(dig +short -t a ${reverse}.${BL}.)" 47 | echo ${HIT:----} 48 | fi 49 | done 50 | } 51 | 52 | # From the comand line or from stdin 53 | { 54 | [ "$#" -gt 0 ] && printf '%s\n' "$@" 55 | [ ! -t 0 ] && cat 56 | } | 57 | while IFS= read -r; do 58 | lookupIP "$REPLY" 59 | #printf 'Got "%s"\n' "$REPLY" 60 | done 61 | 62 | exit; 63 | -------------------------------------------------------------------------------- /src/build_mail_ipset.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | local $ENV{PATH} = "/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/bin:$ENV{PATH}"; 4 | 5 | #use diagnostics; 6 | #use warnings; 7 | #use strict; 8 | 9 | # 10 | # # Author: Jim Dunphy 11 | # License (ISC): It's yours. Enjoy 12 | # Date: 04/28/2019 13 | # 14 | # Example program to allow ip blocking against attackers using ipsets. 15 | # Will put any ip addresses into a blacklist to expire in 1 day if it matches some 16 | # number of bad to times. Can accept multiple files. 17 | # 18 | # usage: Choose 1 of either modes to run it. 19 | # 20 | # debug mode (parse files): 21 | # % zcat -f /opt/zimbra/log/nginx.access* | build_mail_ipset.pl 22 | # % grep '03/May' /opt/zimbra/log/nginx.access.log | build_mail_ipset.pl 23 | # % check_attacks.pl --pstatus=400 | build_mail_ipset.pl 24 | # daemon mode (tail mode) needs to be running as root: 25 | # # build_mail_ipset.pl -t 26 | # 27 | # CAVEAT: To add an ip address into the kernel ipset requires this the ipset command operate as root. 28 | # 29 | # examples: 30 | # % sudo build_mail_ipset.pl -t & 31 | # % zcat -f /opt/zimbra/log/nginx.access* | build_mail_ipset.pl 32 | # if you like the output then executed it 33 | # % zcat -f /opt/zimbra/log/nginx.access* | build_mail_ipset.pl | sh 34 | # To view any ip's adding 35 | # % sudo ipset list 36 | # 37 | # Configure by following STEP0 .. STEP3 below 38 | # 39 | # Note: I do not run this program as I have more sosphisticated methods. 40 | # I show an example using 400 status codes but other logic could be 41 | # added to make this more realistic. 42 | # 43 | 44 | #------------------------------------------------- 45 | # STEP0: 46 | # 47 | # This program requires the Multi File Tail 48 | # Location: 49 | # http://search.cpan.org/~atripps/File-Tail-Multi-0.1/Multi.pm 50 | # https://metacpan.org/pod/release/ATRIPPS/File-Tail-Multi-0.1/Multi.pm 51 | # 52 | # To install perl Multi tail modules 53 | # % wget https://cpan.metacpan.org/authors/id/A/AT/ATRIPPS/File-Tail-Multi-0.1.tar.gz 54 | # % tar zxvf File-Tail-Multi-0.1.tar.gz 55 | # % cd File-Tail-Multi-0.1 56 | # % perl Makefile.pl 57 | # % sudo make install 58 | # 59 | #------------------------------------------------- 60 | 61 | # STEP1: Configuration 62 | 63 | # Force failure if they don't configure this. 64 | local $TRUSTED="127.0.0.1|X.X.X.X|Y.Y.Y.Y"; 65 | #$TRUSTED=CONFIGURE_ME 66 | # how many chances they get in 24 hours before we add them to an ipset 67 | local $badLookUps=1; 68 | 69 | # STEP2: Configure/install ipset. Create ipset, Add a single rule. 70 | # % sudo ipset create blacklist24hr hash:ip hashsize 4096 timeout 86400 71 | # % sudo iptables -A INPUT -m set --set blacklist24hr src -j DROP 72 | # or (don't show as filtered for scans) 73 | # % sudo iptables -A INPUT -m set --set blacklist24hr src -j REJECT --reject-with tcp-reset 74 | # 75 | # Note: adjust timeout for longer duration... 1 week perhaps? 60*60*24*7days = 604800 76 | # timeout is when the ip address is automatically removed by the ipset 77 | # This script will renew those ip's so an active attacker will stay in the ipset longer 78 | # 79 | 80 | use Sys::Syslog; 81 | use File::Tail::Multi; 82 | 83 | #default is to use stdin 84 | my($useSTDIN) = 1; 85 | 86 | #use tail instead 87 | if (shift(@ARGV) =~ "-t") 88 | { 89 | $useSTDIN = 0; 90 | } 91 | 92 | # pipeline 93 | sub ReadStdin { 94 | 95 | while() { 96 | ProcessLine($_); 97 | } 98 | } 99 | 100 | # read build-in logfiles 101 | sub ReadTail { 102 | 103 | $tail=File::Tail::Multi->new ( 104 | OutputPrefix => 0, # 'f' --- would put filename in input stream 105 | RemoveDuplicate => 1, 106 | NumLines => 1, 107 | MaxAge => 15, 108 | ScanForFiles => 30, 109 | #Files => ["/var/log/all.log","/var/log/httpd/access_log","/vendor/apache/clients/www.example.com/logs/www.log"] 110 | Files => ["/opt/zimbra/log/nginx.access.log"] 111 | ); 112 | 113 | # watch multiple log files and process lines of interest 114 | while(1) { 115 | 116 | my $rFD = $tail->read; 117 | 118 | foreach my $FH ( @{$rFD->{FileArray}} ) { 119 | foreach my $LINE ( @{$FH->{LineArray}} ) { 120 | ProcessLine($LINE) if ($LINE ne ''); #only call with data in the log 121 | } 122 | } 123 | sleep 30; 124 | } 125 | } 126 | 127 | # syslog goes to mail.info on ipset addition 128 | openlog("build_mail_ipset.pl", 'ndelay', 'mail'); 129 | 130 | %ip_list = (); 131 | 132 | 133 | # filter line and find ip's to blacklist 134 | sub ProcessLine { 135 | my($line) = @_; 136 | $_ = $line; 137 | 138 | # parse (specific to zimbra nginx.access.log format) 139 | my($ip, $port, $remuser, $date, $request, $status, $bytes, $referrer, $uagent, $upstream) = /^([^:\s]+):?(\d*)\s+(?:-\s)([^\s]+)\s+\[([^\s+]+)[^\]]+\]\s+"([^"]*)"\s+(\d+)\s(\d+)\s+"([^"]*)"\s"([^"]*)"\s+"([^"]*)"/is; 140 | 141 | if (($ip =~ m/$TRUSTED/)) { return }; # Never add our own trusted ip space. 142 | if ($status eq '200') { return }; # Never for normal status 143 | 144 | 145 | # if this ip has previously had a 400, we will count 404's as a dangerous now. 146 | $ip_list{$ip}{'400'}{'count'}++ if (($status eq '404') && (exists $ip_list{$ip}{'400'}{'count'})); 147 | 148 | # if this request has previously generated a '400' on another ip address, we consider this ip dangerous 149 | $ip_list{$ip}{'400'}{'count'} = $badLookUps+1 if (exists $ip_list{$request}); 150 | 151 | # if we get a 400 status, lets remember this request so new ip's can be targetted immediately. 152 | $ip_list{$request}++ if ($status eq '400'); 153 | 154 | # track by ip address, status, and a count 155 | $ip_list{$ip}{$status}{'count'}++; 156 | print "attacker $ip and count is $ip_list{$ip}{$status}{'count'}\n"; 157 | 158 | # Example of blocking any ip that issues too many 400's that are larger than our badLookUps threashold 159 | if (exists $ip_list{$ip}{'400'}{'count'} && $ip_list{$ip}{'400'}{'count'} > $badLookUps) 160 | { 161 | BlockIP($ip, $ip_list{$ip}{'400'}{'count'}); 162 | } 163 | } 164 | 165 | # log ip and add to blacklist 166 | sub BlockIP { 167 | my ($ip,$count) = @_; 168 | 169 | printf ("[%4d] - %s\n", $count,$ip); 170 | printf ("ipset add blacklist24hr %s -exists\n",$ip); 171 | #STEP 3 (uncomment this out) 172 | #system("ipset add blacklist24hr $ip -exist"); 173 | #syslog('info',"ipset add blacklist24hr $ip"); 174 | } 175 | 176 | # main() 177 | if ($useSTDIN) 178 | { 179 | ReadStdin; 180 | } 181 | else 182 | { 183 | ReadTail; 184 | } 185 | 186 | exit; 187 | -------------------------------------------------------------------------------- /src/build_zimbra.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Author: J Dunphy 3/14/2024 5 | # 6 | # Purpose: Build a zimbra FOSS version based on latest tags in the Zimbra FOSS github for version 8.8.15, 9.0.0 or 10.0.0 7 | # The end result is there will be a tarball inside the BUILDS directory that can be installed which contains a install.sh script 8 | # 9 | # Documentation: https://wiki.zimbra.com/wiki/JDunphy-CompileZimbraScript 10 | # 11 | # CAVEAT: Command option --init needs to run as root. Script uses sudo and prompts user when required. 12 | # 13 | # %%% 14 | # --tags,--tags9, --tags8 do not work with dry-run. The issue is that we have a cached version of the tags. To generate new tags, takes 15 | # some time but would be required to figure out tags for a --dry-run for example. Therefore, we exit on tags eventhough --dry-run 16 | # was specified. We will however have a new cached list of tags that future builds can use. Chicken/Egg problem for --dry-run. 17 | # 18 | # .build.builder file is populated with a starting alphanumeric string provided using the --builder option or defaulting to FOSS if none is provided 19 | # builder can be changed at any time using --builder alphanumeric 20 | # .build.number file is populated with a starting build in the format IIInnnn where III is a three digit builder id number, greater 21 | # than 100 to avoid dropping digits, and nnnn is a starting baseline build counter. e.g. 1011000 22 | # The number will be incremented before the build so the first build would be 1010001. File will be created 23 | # automatically but can also be done manually. builderID can be changed at any time using --builderID \d\d\d 24 | # 25 | # Registered Builders 26 | # 101 - FOSS and build_zimbra.sh 27 | # 102 - VSherwood 28 | # 103 - JDunphy 29 | # 150 - Generic 30 | # 31 | # Edit: V Sherwood 4/5/2024 32 | # Enhance script so that specific releases can be requested rather than just the latest release of a particular Zimbra series 33 | # J Dunphy 4/16/2024 Ref: https://forums.zimbra.org/viewtopic.php?p=313419#p313419 34 | # --builder switch and some code recommended from V Sherwood. 35 | # V Sherwood 4/18/2024 36 | # Store .build.number, and add the Requested Tag, git Cloned Tag and Builder Identifier to BUILD_RELEASE 37 | # J Dunphy 4/21/2024 38 | # Cleanup and addition of --builderID 39 | # V Sherwood 4/22/2024 40 | # Store .build.builder, default to FOSS if file not found and --builder option not supplied 41 | # Allow --clean to be specified with --version 42 | # 43 | 44 | scriptVersion=1.18 45 | copyTag=0.0 46 | default_builder="FOSS" 47 | default_number=1011000 48 | build_number_file=".build.number" 49 | builder_name_file=".build.builder" 50 | debug=0 51 | 52 | function d_echo() { 53 | if [ "$debug" -eq 1 ]; then 54 | echo "$@" 55 | fi 56 | } 57 | 58 | 59 | # 60 | # build.pl uses this construct to clone repositories 61 | # % git clone --depth=1 -b 10.0.0-GA repo_name.git repo_dir 62 | # A problem arises when there are multiple tags for the same detached head state. 63 | # examples: zm-charset 64 | # 65 | function show_repository_clone_tag() { 66 | 67 | if [ ! -d zm-zcs ]; then 68 | echo "You need to build a version before running this" 69 | echo " try: $0 --version 10" 70 | exit 1 71 | fi 72 | 73 | # Header for the output 74 | printf "%-20s %-30s %-20s\n" "Tag Name" "Formatted Date" "Directory" 75 | 76 | for dir in zm* ja* neko* ant* ical* 77 | do 78 | 79 | # %%% 80 | # Could not find git command line option to determine which tag the repository was cloned with 81 | # when multiple tags at the same detached head. Which one is correct? 82 | # they use this construct: git clone --depth=1 -b 10.0.0-GA repo_name.git repo_dir 83 | # example: zm-charset showing this issue. 84 | # a grep showed that .git/config contained the tag used in the clone. 85 | line=$(grep -R 'fetch = +refs/tags/' "$dir/.git/config") 86 | repo=$(echo $line | cut -d'/' -f1) # Extracts the repository directory name 87 | tag=$(echo $line | sed -n 's/.*refs\/tags\/\([^:]*\).*/\1/p') # Extracts the tag name 88 | 89 | cd $dir 90 | # An oddcase where the above observation didn't work and does work as expected 91 | if [ $dir == "zm-mailbox" ]; then 92 | tag=$(git describe --tags --exact-match) 93 | fi 94 | read -r timestamp tagname <<< $(git tag --format='%(creatordate:unix)%09%(refname:strip=2)' --sort=-taggerdate | grep "$tag" | head -n 1) 95 | formatted_date=$(date -d "@$timestamp" '+%Y-%m-%d %H:%M:%S') # Formats the timestamp into a human-readable date 96 | cd .. 97 | 98 | printf "%-20s %-30s %-20s\n" "$tag" "$formatted_date" "$dir" 99 | done 100 | } 101 | 102 | # show the latest tag with each repository 103 | function show_repository_tags() { 104 | 105 | if [ ! -d zm-zcs ]; then 106 | echo "You need to build a version before running this" 107 | echo " try: $0 --version 10" 108 | exit 1 109 | fi 110 | 111 | # Header for the output 112 | printf "%-20s %-30s %-20s\n" "Tag Name" "Formatted Date" "Directory" 113 | 114 | for dir in zm* ja* neko* ant* ical* 115 | do 116 | cd $dir 117 | # Get the most recent tag and its creation timestamp 118 | read -r timestamp tagname <<< $(git tag --format='%(creatordate:unix)%09%(refname:strip=2)' --sort=-taggerdate | head -n 1) 119 | 120 | # Convert Unix timestamp to a human-readable date 121 | formatted_date=$(date -d @$timestamp '+%Y-%m-%d %H:%M:%S') 122 | 123 | # Output the values in tabular format, putting the directory name last 124 | printf "%-20s %-30s %-20s\n" "$tagname" "$formatted_date" "$dir" 125 | cd .. 126 | done 127 | } 128 | 129 | # read the first line from a file and set builder 130 | function read_builder() { 131 | 132 | # if we don't have a .build.builder then create the default file 133 | if [ ! -f "$builder_name_file" ]; then update_builder; fi 134 | 135 | # establish the builder for this 136 | if [ -f "$builder_name_file" ]; then 137 | # Read the first line of the file, confirm it is alpha-numreic 138 | builder=$(head -n 1 "$builder_name_file") 139 | d_echo "Found builder is $builder_id" 140 | if [ -z "$builder" ]; then 141 | echo "No alpha-numeric builder name found at the start of the file." 142 | #return 1 # Return a non-zero status to indicate failure 143 | exit 144 | fi 145 | fi 146 | } 147 | 148 | # update .build.builder or create it with defaults 149 | function update_builder() { 150 | if [ ! -f "$builder_name_file" ]; then 151 | # File does not exist, create it and populate it with the default builder 152 | echo $default_builder > $builder_name_file 153 | else 154 | # File exists, overwrite it with $builder 155 | echo $builder > $builder_name_file 156 | fi 157 | } 158 | 159 | # validate input is alphanumeric 160 | function is_alphanumeric() { 161 | if [[ "$1" =~ [^a-zA-Z0-9] ]]; then 162 | return 1 163 | fi 164 | return 0 165 | } 166 | 167 | # read the first three digits from a file and set builder_id 168 | function read_builder_id() { 169 | 170 | # if we don't have a builder_id then create the default and file 171 | if [ ! -f "$build_number_file" ]; then update_builder_no; fi 172 | 173 | # establish the builder_id for this 174 | if [ -f "$build_number_file" ]; then 175 | # Read the first line of the file, extract the first three digits 176 | builder_id=$(head -n 1 "$build_number_file" | grep -o '^[0-9]\{3\}') 177 | d_echo "Found builder_id is $builder_id" 178 | if [ -z "$builder_id" ]; then 179 | echo "No three-digit number found at the start of the file." 180 | #return 1 # Return a non-zero status to indicate failure 181 | exit 182 | fi 183 | fi 184 | } 185 | 186 | # update .build.number or create it with defaults 187 | function update_builder_no() { 188 | if [ ! -f "$build_number_file" ]; then 189 | # File does not exist, create it and populate it with the default number 190 | echo $default_number > $build_number_file 191 | else 192 | # File exists, replace the first three digits with the value of $builder_id 193 | sed -i "s/^[0-9]\{3\}/$builder_id/" $build_number_file 194 | fi 195 | } 196 | 197 | # validate input is a three-digit number - with first digit non-zero 198 | function is_three_digit_number() { 199 | case $1 in 200 | [1-9][0-9][0-9]) return 0 ;; # exactly three digits, with first digit non-zero 201 | *) 202 | return 1 ;; # not exactly three digits, or first digit is zero 203 | esac 204 | } 205 | 206 | # %%% no longer used 207 | function find_tag() { 208 | # find tag that we cloned the zm-build with 209 | if [ -d "zm-build" ] ; then 210 | pushd zm-build 211 | 212 | # Get the current branch name 213 | copyTag=$(git describe --tags --exact-match) 214 | 215 | # Print the current branch 216 | echo "Current branch is: $copyTag" 217 | popd 218 | fi 219 | } 220 | 221 | # Fine the latest zm-build we can check out 222 | function clone_until_success() { 223 | local tags=$1 224 | local repo_url=$2 225 | 226 | IFS=',' read -ra TAG_ARRAY <<< "$tags" 227 | for tag in "${TAG_ARRAY[@]}"; do 228 | echo "Attempting to clone branch $tag..." 229 | if git clone --depth 1 --branch "$tag" "git@github.com:Zimbra/zm-build.git"; then 230 | echo "Successfully cloned branch $tag" 231 | echo "git clone --depth 1 --branch $tag git@github.com:Zimbra/zm-build.git" 232 | copyTag=$tag 233 | return 234 | else 235 | echo "Failed to clone branch $tag. Trying the next tag..." 236 | fi 237 | done 238 | 239 | echo "All attempts failed. Unable to clone the repository with the provided tags." 240 | } 241 | 242 | # Tools that make this possible 243 | function clone_if_not_exists() { 244 | # Extract the repo name from the URL 245 | repo_name=$(basename "$1" .git) 246 | 247 | # Check if the directory already exists 248 | if [ -d "$repo_name" ]; then 249 | echo "Repository $repo_name already exists locally." 250 | return 251 | else 252 | # Clone the repository 253 | git clone "$1" 254 | echo "Repository $repo_name cloned successfully." 255 | fi 256 | } 257 | 258 | # Run one time only 259 | function init () 260 | { 261 | # Get supporting scripts that we will use 262 | clone_if_not_exists https://github.com/ianw1974/zimbra-build-scripts 263 | clone_if_not_exists https://github.com/maldua/zimbra-tag-helper 264 | 265 | # We need another filter script for verison 8.8.15. 266 | cp zimbra-tag-helper/zm-build-filter-tags-9.sh zimbra-tag-helper/zm-build-filter-tags-8.sh 267 | sed -i 's/MAIN_BRANCH="9.0"/MAIN_BRANCH="8.8.15"/' zimbra-tag-helper/zm-build-filter-tags-8.sh 268 | 269 | echo "Will need to run next command as root to install system dependicies and tools" 270 | sudo zimbra-build-scripts/zimbra-build-helper.sh --install-deps 271 | } 272 | 273 | 274 | function usage() { 275 | echo " 276 | !!!NOTE!!! 277 | There is a newer version (2.0+) that is simpler to use and will build all versions of Zimbra 278 | See this link: https://wiki.zimbra.com/wiki/JDunphy-CompileZimbraScript 279 | 280 | % $0 281 | --init #first time to setup envioroment (only once) 282 | --version [10|9|8] #build release 8.8.15 or 9.0.0 or 10.0.0 283 | --version 10.0.8 #build release 10.0.8 284 | --debug #extra output 285 | --clean #remove everything but BUILDS 286 | --tags #create tags for version 10 287 | --tags8 #create tags for version 8 288 | --tags9 #create tags for version 9 289 | --upgrade #echo what needs to be done to upgrade the script 290 | --builder foss # an alphanumeric builder name, updates .build.builder file with value 291 | --builderID [\d\d\d] # 3 digit value starting at 101-999, updates .build.number file with value 292 | -V #version of this program 293 | --dry-run #show what we would do 294 | --show-tags #show latest tag for each repositories 295 | --show-tags | grep 10.0.8 #show latest tag for each repositories with 10.0.8 296 | --show-cloned-tags #show tag of each cloned repository used for build 297 | --help 298 | 299 | Example usage: 300 | $0 --init # first time only 301 | $0 --upgrade # show how get latest version of this script 302 | $0 --upgrade | sh # overwrite current version of script with latest version from github 303 | $0 --version 10 # build latest patch version 10 according to tags 304 | $0 --version 10.0.6 # build version 10.0.6 305 | 306 | $0 --clean; $0 --version 9 #build version 9 leaving version 10 around 307 | $0 --clean; $0 --version 8 #build version 8 leaving version 9, 10 around 308 | $0 --clean; $0 --version 10 --dry-run #see how to build version 10 309 | $0 --clean; $0 --version 10 #build version 10 310 | 311 | WARNING: ******************************************************************************** 312 | the tags are cached. If a new release comes out, you must explicity do this before building if you are using the same directory: 313 | 314 | $0 --clean; $0 --tags 315 | 316 | This is because the tags are cached in a file and need to recalculated again. 317 | ***************************************************************************************** 318 | " 319 | } 320 | 321 | function isRoot() { 322 | # need to run as root because local cache has perm problem 323 | ID=`id -u` 324 | if [ "x$ID" != "x0" ]; then 325 | echo "Run as root!" 326 | exit 1 327 | fi 328 | } 329 | 330 | function get_tags () 331 | { 332 | # requires that it be run in the local directory 333 | pushd zimbra-tag-helper 334 | # %%% not sure but thinking in the future for version 10.1.0 335 | if [ -d "zm-build" ] ; then /bin/rm -rf zm-build; fi 336 | ./zm-build-filter-tags-10.sh > ../tags_for_10.txt 337 | /bin/rm -rf zm-build 338 | popd 339 | } 340 | 341 | function get_tags_9 () 342 | { 343 | # requires that it be run in the local directory 344 | pushd zimbra-tag-helper 345 | if [ -d "zm-build" ] ; then /bin/rm -rf zm-build; fi 346 | ./zm-build-filter-tags-9.sh > ../tags_for_9.txt 347 | /bin/rm -rf zm-build 348 | popd 349 | } 350 | 351 | function get_tags_8 () 352 | { 353 | # requires that it be run in the local directory 354 | # This is EOL already 355 | pushd zimbra-tag-helper 356 | ./zm-build-filter-tags-8.sh > ../tags_for_8.txt 357 | # odd case of how we do release_no 358 | echo ',8.8.15' >> ../tags_for_8.txt 359 | /bin/rm -rf zm-build 360 | popd 361 | } 362 | 363 | function strip_newer_tags() 364 | { 365 | # Trims off the leading tags that are newer than the requested release 366 | 367 | # Add [,] to both strings to avoid matching extended release numbers. e.g. 10.0.0-GA when searching for 10.0.0, or 9.0.0.p32.1 when searching for 9.0.0.p32 368 | tagscomma="$tags," 369 | releasecomma="$release," 370 | 371 | # earlier_releases will either contain the entire tags string if the requested release wasn't found 372 | # or the tail of the tags string after the requested release (which could be nothing if the earliest release was requested) 373 | earlier_releases=${tagscomma#*$releasecomma} 374 | 375 | if [ -n "${earlier_releases}" ]; then 376 | # Some earlier releases in tags - strip the [,] we added for searching 377 | earlier_releases=${earlier_releases%?} 378 | fi 379 | 380 | if [ "$tags" == "$earlier_releases" ]; then 381 | # If earlier_releases contains everything then the requested release does not exist 382 | echo "Bad release number requested - $release!" 383 | echo "You must specify a release number from the tag list: $tags" 384 | echo "If a recent zimbra release is not in the tags list then re-run the script with option" 385 | echo " --tags/--tags9/--tags8 as appropriate to update your local tags_for_nn.txt file" 386 | exit 0 387 | else 388 | if [ -n "$earlier_releases" ]; then 389 | # There are earlier_releases. Append release[,]earlier_releases to make new tags string for building 390 | tags="$release,$earlier_releases" 391 | else 392 | # There are no earlier_releases. Set tags string to release for building 393 | tags="$release" 394 | fi 395 | echo "Building $release!" 396 | echo "Tags for build: $tags" 397 | fi 398 | } 399 | 400 | # Pads version components from 'release' and zm_build branch represented by 'copyTag' to two digits and constructs 401 | # formatted 'Build Tag' and 'Clone Tag'. 402 | function zero_pad_tag_and_clone_versions() 403 | { 404 | # check if a specific release version was requested - Format n.n.n[.p[.n]] 405 | 406 | echo "Release $release" 407 | IFS='.' read -ra version_array <<< "$release" 408 | major="00${version_array[0]}" 409 | minor="00${version_array[1]}" 410 | patch="00${version_array[2]}" 411 | build_tag="${major: -2}${minor: -2}${patch: -2}${version_array[3]}${version_array[4]}" 412 | echo "Build Tag $build_tag" 413 | echo "CopyTag $copyTag" 414 | IFS='.' read -ra version_array <<< "$copyTag" 415 | major="00${version_array[0]}" 416 | minor="00${version_array[1]}" 417 | patch="00${version_array[2]}" 418 | clone_tag="${major: -2}${minor: -2}${patch: -2}${version_array[3]}${version_array[4]}" 419 | echo "Clone Tag $clone_tag" 420 | } 421 | 422 | #====================================================================================================================== 423 | # 424 | # main program logic starts here 425 | # 426 | #====================================================================================================================== 427 | 428 | dryrun=0 429 | args=$(getopt -l "init,show-tags,show-cloned-tags,dry-run,tags,tags8,tags9,help,clean,upgrade,version:,builder:,builderID:,debug" -o "hV" -- "$@") 430 | eval set -- "$args" 431 | 432 | # Now process each option in a loop 433 | while [ $# -ge 1 ]; do 434 | case "$1" in 435 | --) 436 | # No more options left. 437 | shift 438 | break 439 | ;; 440 | --init) 441 | init 442 | exit 0 443 | ;; 444 | --show-tags) 445 | show_repository_tags 446 | exit 0 447 | ;; 448 | --show-cloned-tags) 449 | show_repository_clone_tag 450 | exit 0 451 | ;; 452 | --debug) 453 | debug=1 454 | shift 455 | ;; 456 | --builderID) 457 | if [ -z "$2" ] || ! is_three_digit_number "$2"; then 458 | echo "Error: --builderID requires a three-digit numeric argument, with the first digit non-zero." 459 | exit 1 460 | fi 461 | builder_id=$2 462 | update_builder_no # will create if doesn't exist 463 | shift 2 464 | ;; 465 | --upgrade) 466 | echo cp $0 $0.$scriptVersion 467 | echo wget -O $0 'https://raw.githubusercontent.com/JimDunphy/ZimbraScripts/master/src/build_zimbra.sh' 468 | exit 0 469 | ;; 470 | --dry-run) 471 | dryrun=1 472 | shift 473 | ;; 474 | -V) 475 | echo "Version: $scriptVersion" 476 | exit 0 477 | ;; 478 | --clean) 479 | # currently removing zm-build in explict tags,tags9 option. What about --dry-run? 480 | clean=true 481 | echo "Cleaning up ..." 482 | /bin/rm -rf zm-* j* neko* ant* ical* .staging* 483 | echo "Done!" 484 | shift 485 | ;; 486 | --version) 487 | version=$2 488 | shift 2 489 | ;; 490 | --builder) 491 | if [ -z "$2" ] || ! is_alphanumeric "$2"; then 492 | echo "Error: --builder requires an alphanumeric argument." 493 | exit 1 494 | fi 495 | builder=$2 496 | update_builder # will create if doesn't exist 497 | shift 2 498 | ;; 499 | --tags) 500 | get_tags 501 | exit 0 502 | ;; 503 | --tags9) 504 | get_tags_9 505 | exit 0 506 | ;; 507 | --tags8) 508 | get_tags_8 509 | exit 0 510 | ;; 511 | -h|--help) 512 | usage 513 | exit 0 514 | ;; 515 | esac 516 | done 517 | 518 | # %%% bug... --builder and --builderID will be updated even with dry-run. It happens in the switch statement. 519 | # Processing continues with the only possible options to get here: --builder, --builderID, --clean, --version 520 | 521 | # builderID and/or builder and/or clean should exit if they are not building a version. 522 | if [[ (-n "$builder_id" || -n "$builder" || -n "$clean") && -z "$version" ]]; then 523 | d_echo "quietly exiting as we only want to clean or set builder id/builder" 524 | exit 0 525 | fi 526 | 527 | if [[ -z "$version" ]]; then 528 | echo "build_zimbra.sh: Version not specified" 529 | echo "Try 'build_zimbra.sh --help' for more information." 530 | exit 1 531 | fi 532 | 533 | # check if a specific release version was requested - Format n.n.n[.p[.n]] 534 | IFS='.' read -ra version_array <<< "$version" 535 | major="${version_array[0]}" 536 | minor="${version_array[1]}" 537 | rev="${version_array[2]}" 538 | 539 | if [ -z "${minor}" ] && [ -z "${rev}" ]; then 540 | d_echo "Requested latest Zimbra $major release" 541 | else 542 | release="${version}" 543 | version="${major}" 544 | d_echo "Requested Zimbra $release release" 545 | fi 546 | 547 | # tags is a comma seperated list of tags used to make a release to build 548 | case "$version" in 549 | 8) 550 | if [ ! -f tags_for_8.txt ]; then get_tags_8; fi 551 | tags="$(cat tags_for_8.txt)" 552 | if [ -n "$release" ]; then 553 | strip_newer_tags 554 | else 555 | release=$(echo "$tags" | cut -d ',' -f 1) 556 | fi 557 | LATEST_TAG_VERSION=$(echo "$tags" | awk -F',' '{print $NF}') 558 | PATCH_LEVEL="GA" 559 | BUILD_RELEASE="JOULE" 560 | ;; 561 | 9) 562 | if [ ! -f tags_for_9.txt ]; then get_tags_9; fi 563 | tags="$(cat tags_for_9.txt)" 564 | if [ -n "$release" ]; then 565 | strip_newer_tags 566 | else 567 | release=$(echo "$tags" | cut -d ',' -f 1) 568 | fi 569 | LATEST_TAG_VERSION=$(echo "$tags" | awk -F',' '{print $NF}') 570 | PATCH_LEVEL="GA" 571 | BUILD_RELEASE="KEPLER" 572 | ;; 573 | 10) 574 | if [ ! -f tags_for_10.txt ]; then get_tags; fi 575 | tags="$(cat tags_for_10.txt)" 576 | if [ -n "$release" ]; then 577 | strip_newer_tags 578 | else 579 | release=$(echo "$tags" | cut -d ',' -f 1) 580 | fi 581 | LATEST_TAG_VERSION=$(echo "$tags" | cut -d ',' -f 1) 582 | PATCH_LEVEL="GA" 583 | BUILD_RELEASE="DAFFODIL" 584 | ;; 585 | *) 586 | echo "Possible values: 8 or 9 or 10" 587 | exit 588 | ;; 589 | esac 590 | 591 | # pass these on to the Zimbra build.pl script 592 | # 10.0.0 | 9.0.0 | 8.8.15 are possible values 593 | TAGS_STRING=$tags 594 | 595 | # If zm-build folder exists, --clean wasn't run, build will fail, so abort. unless dry-run where we are not building 596 | if [ -d zm-build ]; then 597 | if [ "$dryrun" -eq 0 ]; then 598 | echo "You must run the script with --clean option before each new build (even if rebuilding the same version)" 599 | echo "The zm-build process will fail if this is not done!" 600 | exit 1 601 | fi 602 | echo "Removing zm-build directory..." 603 | /bin/rm -rf zm-build 604 | fi 605 | 606 | clone_until_success "$tags" >/dev/null 2>&1 607 | 608 | # pads release version and zm_build branch to two digits and constructs formatted $build_tag and $clone_tag 609 | zero_pad_tag_and_clone_versions 610 | 611 | #--------------------------------------------------------------------- 612 | # .build.builder file contains the alphanumeric builder name to appear in the build log 613 | # If this file doesn't exist then create the file using the default builder name FOSS 614 | # If the file does exist then set $builder to the string in the first line 615 | #--------------------------------------------------------------------- 616 | read_builder 617 | # Add the Requested Tag, git Cloned Tag and Builder Identifier to BUILD_RELEASE. 618 | # This will be used in naming the .tgz file output 619 | BUILD_RELEASE="${BUILD_RELEASE}_T${build_tag}C${clone_tag}$builder" 620 | 621 | #--------------------------------------------------------------------- 622 | # .build.number file contains 7 digits. The first 3 digits represent the builder_id and the other 4 the build no. 623 | # This file is passed to zm-build/build.pl that will look for it or creates it if absent. It will +1 increment contents. 624 | # If this file doesn't exist then create a default entry represent FOSS builder id + starting build no. 625 | # If the file does exist then set $builder_id to value of the first 3 digits 626 | #--------------------------------------------------------------------- 627 | read_builder_id 628 | 629 | # Build the source tree with the specified parameters 630 | if [ $dryrun -eq 1 ]; then 631 | 632 | cat << _END_OF_TEXT 633 | #!/bin/sh 634 | 635 | git clone --depth 1 --branch "$tag" "git@github.com:Zimbra/zm-build.git" 636 | cd zm-build 637 | ENV_CACHE_CLEAR_FLAG=true ./build.pl --ant-options -DskipTests=true --git-default-tag="$TAGS_STRING" --build-release-no="$LATEST_TAG_VERSION" --build-type=FOSS --build-release="$BUILD_RELEASE" --build-thirdparty-server=files.zimbra.com --no-interactive --build-release-candidate=$PATCH_LEVEL 638 | 639 | 640 | _END_OF_TEXT 641 | 642 | exit 643 | 644 | else 645 | 646 | # Copy .build.number into the cloned zm-build. build.pl will increment the number and save it back before the build starts 647 | cp .build.number zm-build 648 | cd zm-build 649 | ENV_CACHE_CLEAR_FLAG=true ./build.pl --ant-options -DskipTests=true --git-default-tag="$TAGS_STRING" --build-release-no="$LATEST_TAG_VERSION" --build-type=FOSS --build-release="$BUILD_RELEASE" --build-thirdparty-server=files.zimbra.com --no-interactive --build-release-candidate=$PATCH_LEVEL 650 | # Copy this .build.number back to the parent folder so --clean will not wipe it out. 651 | cp ${build_number_file} .. 652 | fi 653 | cd .. 654 | 655 | # Log the build 656 | build="$(cat "$build_number_file")" 657 | build_tgz="$(ls -1 BUILDS | grep FOSS-$build)" 658 | build_ts="$(date +%Y%m%d-%H%M%S)" 659 | if [[ -z "${build_tgz}" ]]; then 660 | build_tgz="Build failed!" 661 | fi 662 | echo "$build_ts $build $build_tgz" >> ./builds.log 663 | # show completed builds 664 | find BUILDS -name \*.tgz -print 665 | 666 | exit 0 667 | -------------------------------------------------------------------------------- /src/check_attacks.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # Author: Jim Dunphy 4 | # License (ISC): It's yours. Enjoy 5 | # Date: 04/28/2019 6 | # 7 | # usage: check_attack.pl [Options] 8 | # 9 | # ====================================================================== 10 | # 11 | # NOTES: ourUser --- need to customize to each install to provide 12 | # best guess at what external attackers are 13 | # Search for STEP 1 in code to customize 14 | # 15 | # Nov 12, 2022 - getting better. Very little customizations needed anymore 16 | # 17 | 18 | #======================================================================== 19 | # SECTION - Modules, Variables, etc. 20 | #======================================================================== 21 | use Term::ANSIColor; 22 | use Data::Dumper qw(Dumper); 23 | use Getopt::Long; 24 | 25 | package main; 26 | 27 | use diagnostics; 28 | use warnings; 29 | use strict; 30 | 31 | my %ip_list = (); #ip list 32 | my %PossibleStatusCodes = (); 33 | my $logDir="/opt/zimbra/log"; 34 | my $nginx_access_log="nginx.access.log*"; 35 | 36 | #======================================================================== 37 | # SECTION - BOTS (listed in user agent) and their bait 38 | #======================================================================== 39 | my $bot_list = "zgrab|Bot|python|curl|lwp|wget|http|parser|cyberwarcon"; 40 | my $bot_bait = "logon\.aspx|jnamespaces|wp-includes|pods|\.jsp"; 41 | my $san_list = ''; 42 | 43 | my $pstatus = ''; #default not to print status codes 44 | my $ipset = 0; #print local ip addresses in ipset format 45 | my $fail2ban = 0; #print ip addresses in fail2ban format 46 | my $fcolor = 'CYAN'; # GREEN, etc 47 | my $display = 'uagent'; # default user agent 48 | my $srcip = '@'; 49 | my $search = ''; 50 | my $statuscnt = 0; #default not to print status codes 51 | my $localUser = 0; #default not to include localusers 52 | my $IPlist = 0; #print ip addresses 53 | my $initIPset = 0; #show how to create an ipset 54 | my ($help, $debug) = 0; 55 | my $usertype = 'attacker'; 56 | 57 | # pre-declare Globals 58 | use vars qw( $debug $localUser $pstatus $fail2ban $ipset $search $display $attacker $usertype $srcip $msgcolor $nginx_log ); 59 | 60 | #======================================================================== 61 | # SECTION - FUNCTIONS 62 | #======================================================================== 63 | # Displays program usage 64 | 65 | my $PROJECT="https://raw.githubusercontent.com/JimDunphy/ZimbraScripts/master/src/check_attacks.pl"; 66 | my $VER="0.9.03"; 67 | 68 | sub version() { 69 | print "wget $PROJECT\n#v$VER\n"; 70 | exit; 71 | } 72 | 73 | sub usage { 74 | 75 | print <<"END"; 76 | usage: % check_attacker.pl 77 | [--fcolor=] 78 | [--srcip=] 79 | [--search='regex of search'] 80 | [--localUser ] 81 | [--logDir=`pwd` ] 82 | [--file=nginx.access.log ] 83 | [--IPlist ] 84 | [--ipset ] 85 | [--fail2ban ] 86 | [--statuscnt] 87 | [--display="date|upstream|bytes|port|referrer] 88 | [--usertype= 89 | [--pstatus= 90 | [--help] 91 | [--version] 92 | where: 93 | --srcip|src: print only records matching ip addresses 94 | --search|sea: print all requests from an ip that has a search term hit 95 | --statuscnt: prints out the count for each status return code found 96 | --help|h: this message 97 | examples: (-- or - or first few characters of option so not ambigous) 98 | % check_attacker.pl --srcip 10.10.10.1 #only this ip address 99 | % check_attacker.pl --srcip '10.10.10.1|20.20.20.2' #only these ip addresses 100 | % check_attacker.pl --search 'python|POST' # if an ip had search term, all requests printed for ip 101 | % check_attacker.pl --search '\.jsp|\.php|bot' # if an ip had search term, all requests printed for ip 102 | % check_attacker.pl --localUser #include local users accounts 103 | % check_attacker.pl --IPlist # print list of ips 104 | % check_attacker.pl --IPlist --localUser # print list of ips from local users 105 | % check_attacker.pl --IPlist --ipset # print list of ips in ipset format 106 | % check_attacker.pl --IPlist --fail2ban # print list of ips in fail2ban format 107 | % check_attacker.pl --IPlist -pstatus='40.' --ipset # print list of ips in ipset format with status code 400..409 108 | % check_attacker.pl --IPlist -pstatus='40.' --fail2ban # print list of ips in fail2ban format with status code 400..409 109 | % check_attacker.pl --localUser --IPlist # print list of local ips used by local users 110 | % check_attacker.pl --IPlist --ipset | sh # format ip's into ipset syntax 111 | % check_attacker.pl --IPlist --fail2ban | sh # format ip's into fail2ban syntax 112 | % check_attacker.pl --initIPset # show how to create ipset 113 | % check_attacker.pl -fc RED #change color 114 | % check_attacker.pl --usertype=local # print out strings of only local users - ignore Attacker and 100% on output line 115 | % check_attacker.pl --pstatus='4..' # print out only those requests with a code of 4XX (ie 403, 404, 499) 116 | % check_attacker.pl --usertype=all --pstatus='403|500' # print out only those requests with a code of 403 or 500 for all types (local & attacker) 117 | % check_attacker.pl --display=date # default is to display the user agent 118 | % check_attacker.pl --display=referrer # default is to display the user agent 119 | % check_attacker.pl --version | sh # pull the latest verstion with wget to current working directory 120 | % check_attacker.pl --statuscnt #print status codes 121 | % check_attacks.pl --logDir=`pwd` --file=nginx.access.log.5 --pstatus='499' --usertype=all # client closed before Server response 122 | 123 | Status Codes - https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 124 | END 125 | exit 0; 126 | } 127 | 128 | 129 | # ./check_attacks.pl --iplist --pstatus='40.' 130 | # ./check_attacks.pl --iplist --pstatus='40.' --ipset 131 | # ./check_attacks.pl --iplist --ipset 132 | # ./check_attacks.pl --iplist 133 | # ./check_attacks.pl --iplist --localUser 134 | 135 | sub printIPs { 136 | 137 | for my $ip (sort keys %ip_list ) { 138 | # print local ips 139 | if ($localUser) { 140 | print "$ip\n" if $ip_list{$ip}{'ourUser'} ; 141 | } 142 | elsif (!exists $ip_list{$ip}{'ourUser'}) # only non-local IPs from here forward 143 | { 144 | my $smatch = 0; 145 | # print by status codes 146 | if ($pstatus ne '') 147 | { 148 | # loop through the status array for this ip address 149 | for (my $i=0; $i < $#{$ip_list{$ip}{'request'}}+1; $i++) 150 | { 151 | my $status = $ip_list{$ip}{'status'}[$i]; 152 | $smatch = 1, last if ($status =~ /$pstatus/); 153 | } 154 | } 155 | # print by search 156 | elsif ($search ne '') 157 | { 158 | $smatch = 1 if $ip_list{$ip}{'tag'}; 159 | } else { 160 | $smatch = 1; 161 | } 162 | # print all the attacking ip's 163 | print "ipset add blacklist24hr $ip -exist\n" if ($ipset && $smatch); 164 | print "fail2ban-client set zimbra-nginx $ip\n" if ($fail2ban && $smatch); 165 | print "$ip\n" if ($smatch && !$fail2ban && !$ipset); 166 | 167 | #($ipset ? print "ipset add blacklist24hr $ip -exist\n" : print "$ip\n") if ($smatch); 168 | #($fail2ban ? print "fail2ban-client set zimbra-nginx $ip\n" : print "$ip\n") if ($smatch); 169 | } 170 | } 171 | } 172 | 173 | sub printIPsetInit { 174 | print <<"END"; 175 | 176 | # - ipset create should be executed only once 177 | # - It requires that you have a single rule like this defined 178 | # 179 | # % sudo iptables -A INPUT -m set --set blacklist24hr src -j DROP 180 | # 181 | # Example using centos/rhel /etc/sysconfig/iptables 182 | # Syntax for /etc/sysconfig/iptables 183 | # -A INPUT -m set --match-set blacklist24hr src -j DROP 184 | # Note: Any ip added to the ipset blacklist24hr will expire in 24 hours 185 | # - use the --ipset --iplist options to format ip's into ipset add commands 186 | # reference: http://ipset.netfilter.org/index.html 187 | 188 | ipset create blacklist24hr hash:ip hashsize 4096 timeout 86400 189 | END 190 | exit; 191 | } 192 | 193 | sub printCodes { 194 | 195 | for my $codes (sort keys %PossibleStatusCodes ) 196 | { 197 | print "Codes $codes Total: $PossibleStatusCodes{$codes}{'count'}\n"; 198 | } 199 | exit; 200 | } 201 | 202 | 203 | sub setlists { 204 | my ($ip, $port, $remuser, $date, $request, $status, $bytes, $referrer, $uagent, $upstream) = @_; 205 | 206 | #%%% BEGIN STEP 1 207 | # noise (filter some of this out) - this won't be saved. 208 | return if ($uagent =~ /uptimerobot/i); # self-induced - If you use a bot to measure uptime 209 | 210 | return if ($request =~ /favicon/i); 211 | # %%% GET / HTTP is a problem with a 200 status. Both users and bots look the same. Need additional information so noise for now 212 | return if (($status eq '200') && ($request =~ m#GET\s+/\s+HTTP#i)); # noise for now 213 | if ($status =~ m#401|403|404|499|500|502#) 214 | { 215 | return if ($request =~ /EWS/i); 216 | return if ($request =~ /Briefcase/i); 217 | return if ($request =~ m#service/home/#i); 218 | return if ($request =~ /apple-touch/i); 219 | return if ($request =~ /ActiveSync/i); # %%% can happen with 401/403/499/500/502 codes 220 | 221 | } 222 | 223 | # Normal Zimbra user stream found with 224 | # check_attack --usertype=local 225 | $ip_list{$ip}{'ourUser'} = 1 if (($status eq '200') && ($request =~ m#(jsessionid|adminPreAuth|st=conversation)#)); 226 | $ip_list{$ip}{'ourUser'} = 1 if (($status eq '200') && ($request =~ m#(ActiveSync)#i)); 227 | $ip_list{$ip}{'ourUser'} = 1 if ((($status eq '200') && $request =~ /POST/) && ($request =~ m#soap|NoOpRequest#i)); 228 | $ip_list{$ip}{'ourUser'} = 1 if (($status =~ m#207|200#) && $request =~ m#REPORT|PROPFIND#i && $request =~ m#dav|users#i); 229 | # more general case with service/soap 230 | $ip_list{$ip}{'ourUser'} = 1 if ((($status eq '200') && $request =~ /POST/) && ($request =~ m#service/soap#)); 231 | 232 | 233 | #%%% END STEP 1 - our own users 234 | 235 | #%%% need to investigate $upstream still... possible hacking 236 | 237 | # skip this attacker, if -search paramater is given 238 | # check_attacks.pl --search 'POST|Auto' 239 | # check_attacks.pl --display=date --search '24/May|Post' 240 | # check_attacks.pl -search '\.jsp|\.php' 241 | if(($search ne '') && (($request.$uagent.$date.$referrer) =~ m#$search#i)) { 242 | $ip_list{$ip}{'tag'} = 1; 243 | } 244 | 245 | # definitely hacking... 246 | ++$ip_list{$ip}{'hack'} if (($request =~ m#^-#) || ($uagent =~ m#^-$#)); 247 | ++$ip_list{$ip}{'hack'} if ($uagent =~ m#$bot_list#i); 248 | ++$ip_list{$ip}{'hack'} if ($request =~ m#$bot_bait#i); 249 | ++$ip_list{$ip}{'hack'} if ($request !~ m#($san_list)#i); 250 | ++$ip_list{$ip}{'hack'} if ($status == '400'); # malformed requests 251 | ++$ip_list{$ip}{'hack'} if ($request =~ m#GET\s+/zimbra\s+HTTP#); # malformed requests 252 | 253 | # clean up if data is missing 254 | $request = 'stealth request - exploit attemped' if ($request =~ m#^$#); 255 | $uagent = 'bot' if ($uagent =~ m#^-$#); 256 | 257 | # no need for HTTP/1.1, etc on request 258 | $request =~ s#HTTP/.*##i if ($request =~ /http/i); 259 | 260 | my $i = ++$ip_list{$ip}{'count'} - 1; 261 | $ip_list{$ip}{'request'}[$i] = $request; # count of requests per ip 262 | $ip_list{$ip}{'status'}[$i] = $status; 263 | 264 | # reuse this second field to display others columns 265 | $ip_list{$ip}{'uagent'}[$i] = $uagent; # default 266 | $ip_list{$ip}{'uagent'}[$i] = $date if ($display =~ m#date#i); 267 | $ip_list{$ip}{'uagent'}[$i] = $upstream if ($display =~ m#upstream#i); 268 | $ip_list{$ip}{'uagent'}[$i] = $bytes if ($display =~ m#bytes#i); 269 | $ip_list{$ip}{'uagent'}[$i] = $port if ($display =~ m#port#i); 270 | $ip_list{$ip}{'uagent'}[$i] = $referrer if ($display =~ m#referrer#i); 271 | 272 | 273 | # Store off status code counts aware of usertype request. This is for the printCode 274 | $PossibleStatusCodes{$status}{'count'}++; 275 | 276 | #print Dumper \%ip_list; 277 | 278 | return; 279 | } 280 | 281 | # Print results out 282 | sub printresults { 283 | my($ucolor, $uattr, $user, $msgcolor, $msg) = @_; 284 | 285 | print color($ucolor, $uattr), " "; 286 | printf "%-47s", $user; 287 | print color('reset'); 288 | print color($msgcolor), $msg; 289 | print color('reset'); 290 | 291 | return; 292 | } 293 | 294 | sub printRequests { 295 | 296 | #debug 297 | #print Dumper \%ip_list; 298 | 299 | # Print out the arrays by attackers ip address. Flag failures. 300 | for $attacker (sort keys %ip_list ) 301 | { 302 | # don't print local/accepted users if only looking for attackers 303 | next if (($ip_list{$attacker}{'ourUser'} && ($usertype eq 'attacker')) 304 | || (($usertype eq 'local') && !$ip_list{$attacker}{'ourUser'})); 305 | 306 | # Skip this attacker, if -srcip parameter is given and attacker is not in search string 307 | # check_attacks.pl --srcip '61.177.26.58|159.69.81.117|45.112.125.139|185.234.217.185|185.234.218.228' 308 | next if ($attacker !~ m#$srcip# && $srcip ne '@'); 309 | 310 | # don't print attackers that didn't match search 311 | next if (! exists $ip_list{$attacker}{'tag'} && $search ne ''); 312 | 313 | my $hitstatus = 0; 314 | my $hack = 0; # %%% The next 3 lines should be moved to setlists 315 | $hack = 25 if (!$ip_list{$attacker}{'ourUser'}); 316 | $hack = 100 if (exists $ip_list{$attacker}{'hack'}); 317 | 318 | # print the requests per ip address 319 | for (my $i=0; $i < $#{$ip_list{$attacker}{'request'}}+1; $i++) 320 | { 321 | my $request = $ip_list{$attacker}{'request'}[$i]; 322 | my $uagent = $ip_list{$attacker}{'uagent'}[$i]; 323 | my $status = $ip_list{$attacker}{'status'}[$i]; 324 | 325 | # printing by status code 326 | next if (($pstatus ne '') && (!($status =~ /$pstatus/))); 327 | $hitstatus++; 328 | 329 | printf ("\t[%4d] %s %s", $status,$request, $uagent); 330 | print color('reset'); 331 | printf ("\n"); 332 | } 333 | 334 | if ($hitstatus) 335 | { 336 | my $msg = sprintf("%d Requests - Score %d%% ", $ip_list{$attacker}{'count'}, $hack); 337 | $msgcolor = $hack > 50 ? "RED" : "CYAN"; 338 | my $userstr = $hack ? "Attacker from " : "Zimbra User from "; 339 | printresults("RED", "BOLD", "$userstr $attacker", $msgcolor, $msg); 340 | drawline(); 341 | } 342 | 343 | } 344 | } 345 | 346 | # Pretty clear - output a big line 347 | sub drawline { 348 | print "\n------------------------------------------------------------------------------------------------------------\n"; 349 | return; 350 | } 351 | 352 | sub getSanList { 353 | 354 | #certificate names 355 | open my $fh, "-|","openssl s_client -connect 127.0.0.1:443 < /dev/null 2> /dev/null | openssl x509 -noout -text | grep DNS:" or die $!; 356 | 357 | my ($san); 358 | $san_list=<$fh>; 359 | 360 | $san_list =~ s#DNS:##gp; 361 | $san_list =~ s#,#|#gp; 362 | $san_list =~ s#\s+##gp; 363 | $san_list =~ s#\.#\\.#gp; 364 | $san_list =~ s#\*#\\*#gp; #wild card domains 365 | 366 | close($fh); 367 | } 368 | 369 | #======================================================================== 370 | # SECTION - GET input parameters 371 | #======================================================================== 372 | # Get the command line parameters for processing 373 | &GetOptions("fcolor=s" => \$fcolor, # %%% ToDo 374 | "display=s" => \$display, 375 | "srcip=s" => \$srcip, 376 | "file=s" => \$nginx_access_log, 377 | "logDir=s" => \$logDir, 378 | "search=s" => \$search, 379 | "debug" => \$debug, # turn on debugging 380 | "localUser" => \$localUser, # turn on localuser 381 | "IPlist" => \$IPlist, # print out ip's in a list format 382 | "ipset" => \$ipset, 383 | "fail2ban" => \$fail2ban, 384 | "initIPset" => \$initIPset, 385 | "pstatus:s" => \$pstatus, # turn on status codes 386 | "statuscnt" => \$statuscnt, # print out count of status codes 387 | "usertype=s" => \$usertype, # print out count of status codes 388 | "version" => \&version, 389 | "help" => \$help); 390 | 391 | # call Help if parameters do not meet expected values or help is requested 392 | $usertype = 'attacker' if $usertype eq ''; 393 | usage() if($help || ($usertype !~ m/^attacker|local|all$/)); 394 | 395 | # determine what are legal names based on the certificate - used to determine bot vs user 396 | getSanList; 397 | 398 | 399 | #======================================================================== 400 | # SECTION - PARSE audit.log nginx_access_logs & process accordingly 401 | #======================================================================== 402 | #chdir "/opt/zimbra/log"; 403 | chdir "$logDir"; 404 | 405 | #for (glob 'nginx.access.log*') { 406 | #for (glob 'nginx.access.log') { 407 | for (glob $nginx_access_log) { 408 | 409 | $nginx_log = $_ eq 'nginx.access.log' ? 1 : 0; 410 | #print "Opening nginx_access_log $_"; 411 | open (IN, sprintf("zcat -f %s |", $_)) 412 | or die("Can't open pipe from command 'zcat -f $nginx_log' : $!\n"); 413 | 414 | while () 415 | { 416 | 417 | my($ip, $port, $remuser, $date, $request, $status, $bytes, $referrer, $uagent, $upstream) = /^([^:\s]+):?(\d*)\s+(?:-\s)([^\s]+)\s+\[([^\s+]+)[^\]]+\]\s+"([^"]*)"\s+(\d+)\s(\d+)\s+"([^"]*)"\s"([^"]*)"\s+"([^"]*)"/is; 418 | 419 | next if (! defined $ip ); # have seen double ip entries in a log so we punt 420 | 421 | #print "ip [$ip] "; 422 | #print "remuser [$remuser] "; 423 | #print "port is [$port] "; 424 | #print "date is [$date] "; 425 | #print "request is [$request] "; 426 | #print "status is [$status] "; 427 | #print "bytes is [$bytes] "; 428 | #print "user_agent is [$uagent] "; 429 | #print "$ip ip referrer is [$referrer] "; 430 | #print "upstream is [$upstream] "; 431 | #print "\n"; next; 432 | 433 | setlists($ip, $port, $remuser, $date, $request, $status, $bytes, $referrer, $uagent, $upstream); 434 | 435 | } 436 | close (IN); 437 | 438 | } 439 | 440 | 441 | #======================================================================== 442 | # SECTION - PRINT / MAIN 443 | #======================================================================== 444 | #debug 445 | #print Dumper \%ip_list; 446 | #print Dumper \%PossibleStatusCodes; 447 | 448 | 449 | # MAIN LOGIC 450 | if($statuscnt) { 451 | printCodes; 452 | } elsif ($initIPset) { 453 | printIPsetInit; 454 | } elsif ($IPlist) { 455 | printIPs; 456 | exit; # we don't want to add garbage with reset if part of a pipeline 457 | } else { 458 | printRequests; 459 | } 460 | 461 | # finished / clean up 462 | printf ("\n"); 463 | print color('reset'); # make sure we clean up 464 | 465 | # END 466 | -------------------------------------------------------------------------------- /src/check_dumpster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Ref: https://forums.zimbra.org/viewtopic.php?p=314569#p314569 4 | # 5 | # Author: ChatGPT 4o 6 | # Human: Jim Dunphy - jad@aesir.com 7 | # 8 | # Check for ZBUG-4613 where disk sizes can expan because IMAP duplication and dumpster expiring out deleted 9 | # email. 10 | # 11 | # Caveat: Should be run as the zimbra user 12 | # 13 | 14 | # Check if an email address is provided 15 | if [ -z "$1" ]; then 16 | echo "Usage: $0 " 17 | exit 1 18 | fi 19 | 20 | # Set the mailbox email 21 | mailbox="$1" 22 | 23 | # Extract the mailbox ID using zmprov 24 | mailboxid=$(/opt/zimbra/bin/zmprov gmi "$mailbox" | grep "mailboxId" | awk '{print $2}') 25 | 26 | # Validate if mailbox ID was retrieved 27 | if [ -z "$mailboxid" ]; then 28 | echo "Error: Could not retrieve mailbox ID for $mailbox." 29 | exit 2 30 | fi 31 | 32 | # Calculate the mailbox group 33 | group=$(expr "$mailboxid" % 100) 34 | 35 | # Validate the group calculation 36 | if [ -z "$group" ]; then 37 | echo "Error: Could not calculate mailbox group." 38 | exit 3 39 | fi 40 | 41 | # Query the MariaDB database for the count of mail items in the dumpster 42 | mysql_output=$(mysql -N -B mboxgroup"$group" -e "SELECT COUNT(*) FROM mail_item_dumpster WHERE mailbox_id=$mailboxid;") 43 | 44 | # Validate the database query 45 | if [ $? -ne 0 ]; then 46 | echo "Error: Database query failed." 47 | exit 4 48 | fi 49 | 50 | # Output the results 51 | echo "Mailbox ID: $mailboxid" 52 | echo "Mailbox Group: mboxgroup$group" 53 | echo "Count of items in dumpster: $mysql_output" 54 | 55 | -------------------------------------------------------------------------------- /src/check_login.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # Author: Jim Dunphy 4 | # License (ISC): It's yours. Enjoy 5 | # Date: 10/9/2015 6 | # Update on feedback from Lapsy from forums.zimbra.com 10/13/2018 7 | # 8 | # usage: check_login.pl [Options] 9 | # 10 | # ====================================================================== 11 | 12 | #======================================================================== 13 | # SECTION - Modules, Variables, etc. 14 | #======================================================================== 15 | use Term::ANSIColor; 16 | use Data::Dumper qw(Dumper); 17 | use Getopt::Long; 18 | use Socket qw( inet_aton AF_INET ); 19 | 20 | %ip_list = (); #ip list 21 | %fip_list = (); #failed ip list 22 | 23 | #======================================================================== 24 | # SECTION - FUNCTIONS 25 | #======================================================================== 26 | # Displays program usage 27 | sub usage { 28 | 29 | print <<"END"; 30 | usage: % check_login.pl 31 | [--color=] 32 | [--srchuser=] 33 | [--fail=] 34 | [--gethost=] 35 | [--help] 36 | where: 37 | --color|c: color to be used for FAILED login message information 38 | --srchuser|s: print ONLY the logins/failed logins for 39 | --fail|f: if 'user': print ONLY users who have failed logins. If 'ip': print ONLY the failed login attempts. 'none': print all records regardless if failure 40 | --gethost|g: values of 'all', 'fail' or 'none'. Perform a GETHOSTBYADDR for all IPs, only on FAILED login attempts, or don't perform this action (none) 41 | --help|h: this message\n"; 42 | example: % check_login.pl -f=user #only the accounts with failed logins 43 | % check_login.pl -f ip #only the accounts and the ip that failed 44 | % check_login.pl -fail=ip # same as above 45 | % check_login.pl --fail ip # same as above 46 | % check_login.pl -f ip -h #list only ip's that failed for accounts resolve ip to domain name 47 | % check_login.pl -g fail #list only accounts that had a failed login 48 | % check_login.pl -g all #list all accounts and resolve ip to domain name 49 | % check_login.pl -c RED -f ip #change color and list only failed ip's 50 | % check_login.pl -s user -f ip -g fail #list all failed ip addresses with ip to domain name 51 | % check_login.pl -s user@example.com -f ip -g fail #list all failed ip addresses with ip to domain name 52 | END 53 | exit 0; 54 | } 55 | 56 | sub setlists { 57 | my ($user, $ip, $typeval) = @_; 58 | 59 | ++$ip_list{$user}{$ip}; #we loop by this for report 60 | ++$fip_list{$user}{$ip}{'count'}; 61 | ++$fip_list{$user}{$ip}{$typeval}; 62 | 63 | return; 64 | } 65 | 66 | # get the hostname for the iP address if requested 67 | sub gethostname { 68 | my ($gethost) = @_; 69 | my $attacker = ""; 70 | 71 | if (($gethost =~ m/all/i) 72 | || (($gethost =~ /fail/i) && ((exists $fip_list{$user}{$ip}) && ($fip_list{$user}{$ip}{count})))) { 73 | if((gethostbyaddr(inet_aton($ip), AF_INET)) =~ /([a-z0-9_\-]{1,5})?(:\/\/)?(([a-z0-9_\-]{1,})(:([a-z0-9_\-]{1,}))?\@)?((www\.)|([a-z0-9_\-]{1,}\.)+)?([a-z0-9_\-]{3,})(\.[a-z]{2,4})(\/([a-z0-9_\-]{1,}\/)+)?([a-z0-9_\-]{1,})?(\.[a-z]{2,})?(\?)?(((\&)?[a-z0-9_\-]{1,}(\=[a-z0-9_\-]{1,})?)+)?/) { 74 | $attacker="$10$11$15"; 75 | } 76 | } 77 | 78 | return $attacker; 79 | } 80 | 81 | # Print results out 82 | sub printresults { 83 | my($ucolor, $uattr, $user, $msgcolor, $msg) = @_; 84 | 85 | print color($ucolor, $uattr), " "; 86 | printf "%-47s", $user; 87 | print color('reset'); 88 | print color($msgcolor), $msg; 89 | print color('reset'); 90 | 91 | return; 92 | } 93 | 94 | # Pretty clear - output a big line 95 | sub drawline { 96 | print "\n------------------------------------------------------------------------------------------------------------\n"; 97 | return; 98 | } 99 | 100 | 101 | #======================================================================== 102 | # SECTION - GET input parameters 103 | #======================================================================== 104 | # Get the command line parameters for processing 105 | my $fcolor = 'YELLOW'; 106 | my $srchuser = '@'; 107 | my $failtype = 'none'; #default failure behavior (user|ip|none) 108 | my $gethost = 'fail'; #default lookup behavior (all|fail|none) 109 | my $help, $dbug = 0; 110 | &GetOptions( "color=s" => \$fcolor, 111 | "srchuser=s" => \$srchuser, 112 | "fail=s" => \$failtype, # user, ip, none 113 | "gethost=s" => \$gethost, # all, fail, none 114 | "debug" => \$dbug, # turn on debugging 115 | "help" => \$help); 116 | 117 | # call Help if parameters do not meet expected values or help is requested 118 | usage() if($help || ($failtype !~ m/^user|ip|none$/i) || ($gethost !~ m/^all|fail|none$/i)); 119 | 120 | 121 | #======================================================================== 122 | # SECTION - PARSE audit.log files & process accordingly 123 | #======================================================================== 124 | chdir "/opt/zimbra/log"; 125 | 126 | for (glob 'audit.log*') { 127 | 128 | $lines = 0; 129 | $audit_log = $_ eq 'audit.log' ? 1 : 0; 130 | #print "Opening file $_"; 131 | open (IN, sprintf("zcat -f %s |", $_)) 132 | or die("Can't open pipe from command 'zcat -f $filename' : $!\n"); 133 | 134 | # part the audit logs looking for access types 135 | # we just doing this 136 | # zcat -f /opt/zimbra/log/audit.log* | grep -i invalid |egrep '(ImapS|Pop|http)' 137 | while () 138 | { 139 | if (m#invalid#i) 140 | { 141 | #print $_; 142 | if ((m#ImapS#i) && !(m#INFO#)) 143 | { 144 | my($ip,$user) = m#.*\s+\[ip=.*;oip=(.*);via=.*;\]\s*.* failed for\s+\[(.*)\].*$#i; 145 | $uagent = "imap"; 146 | #print " - ip is $ip, user is $user, agent is $uagent\n"; 147 | #print $_; 148 | setlists($user, $ip, $uagent); 149 | } 150 | elsif ((m#Pop#i) && !(m#INFO#)) 151 | { 152 | my($ip,$user) = m#oip=(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3});.* failed for\s+\[(.*)\].*$#i; 153 | $uagent = "pop"; 154 | #print " - ip is $ip, user is $user, agent is $uagent\n"; 155 | #print $_; 156 | setlists($user, $ip, $uagent); 157 | } 158 | elsif ((m#http#i) && (m#zclient#)) 159 | { 160 | my($user,$ip,$uagent) = m#.*\s+\[name=(.*);oip=(.*);ua=(.*);\].*$#i; 161 | $uagent = "web"; 162 | #print " - ip is $ip, user is $user, agent is $uagent\n"; 163 | #print $_; 164 | setlists($user, $ip, $uagent); 165 | } 166 | elsif ((m#oproto=smtp#) && (m#failed#)) 167 | { 168 | my($ip,$user) = m#oip=(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3});.* failed for\s+\[(.*)\].*$#i; 169 | $uagent = "smtp"; 170 | #print " - ip is $ip, user is $user, agent is $uagent\n"; 171 | #print $_; 172 | setlists($user, $ip, $uagent); 173 | } 174 | } 175 | elsif (m#AuthRequest#i && ($_ !~ m/zimbra/i)) 176 | { 177 | my($user,$ip,$uagent) = m#.*\s+\[name=(.*);oip=(.*);ua=(.*);\].*$#i; 178 | ++$ip_list{$user}{$ip}; 179 | #if ($audit_log == 1) { print $_; } 180 | #printf "%4d: - ip is %15s, user is %45s, agent is %s\n",$lines,$ip,$user,$uagent; 181 | } 182 | $lines++; 183 | } # End While () loop 184 | close (IN); 185 | 186 | } 187 | 188 | #======================================================================== 189 | # SECTION - PRINT / MAIN 190 | #======================================================================== 191 | #debug 192 | #print Dumper \%ip_list; 193 | #print Dumper \%fip_list; 194 | 195 | 196 | drawline(); 197 | 198 | # Print out the arrays by username. Flag failures. 199 | for $user (sort keys %ip_list ) 200 | { 201 | 202 | # Skip this user, if -s parameter is given and user is not in search string 203 | next if(index($user,$srchuser) == -1); 204 | 205 | # Proceed only if we're only looking for users who have failed logins recorded 206 | next if(($failtype =~ /user|ip/i) && !(exists $fip_list{$user})); 207 | 208 | $total = 0; 209 | $totalf = 0; 210 | 211 | for $ip (sort {$ip_list{$user}{$b} <=> $ip_list{$user}{$a}} keys %{$ip_list{$user}} ) 212 | { 213 | 214 | # See count of how many times 215 | if(($failtype !~ /ip/i) || (($failtype =~ /ip/) && exists $fip_list{$user}{$ip})) { 216 | printf ("[%4d] logins from IP %15s ", $ip_list{$user}{$ip},$ip); 217 | } 218 | $total = $total+$ip_list{$user}{$ip}; # Count all for this username 219 | 220 | # lookup the domain if requested 221 | my $attacker = "[" . gethostname($gethost) . "]"; 222 | 223 | if ((exists $fip_list{$user}{$ip}) && ($fip_list{$user}{$ip}{count})) 224 | { 225 | print color($fcolor); 226 | printf "%-30s", $attacker if ($gethost !~ /none/i); 227 | 228 | printf " Failed [%4d] : ", $fip_list{$user}{$ip}{count}; 229 | for $etypes (keys %{$fip_list{$user}{$ip}}) { 230 | next if $etypes =~ /count/; 231 | printf " using %s [%4d] ", $etypes, $fip_list{$user}{$ip}{$etypes}; 232 | } 233 | print color('reset'); 234 | printf ("\n"); 235 | $totalf = $totalf+$fip_list{$user}{$ip}{count}; # Count all failed for this username 236 | } elsif (($gethost =~ /all/i) && !($failtype =~ /ip/i)) 237 | { 238 | printf "$attacker\n"; 239 | } elsif ($failtype !~ /ip/) { 240 | printf ("\n"); 241 | } 242 | } 243 | 244 | # Print out user information & message totals 245 | if ($totalf>0) { 246 | my $msg = sprintf("%d failed of total %d login attempts!!!", $totalf, $total); 247 | $msgcolor = $totalf==$total ? "RED" : "YELLOW"; 248 | printresults("WHITE", "BOLD", $user, $msgcolor, $msg); 249 | } elsif ($failtype !~ /ip/i) { 250 | my $msg = " No failed logins. Yeeee :)"; 251 | printresults("WHITE", "BOLD", $user, "GREEN", $msg); 252 | } 253 | 254 | drawline(); 255 | } 256 | printf ("\n"); 257 | print color('reset'); # make sure we clean up 258 | -------------------------------------------------------------------------------- /src/check_recipients.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # test of concept... 4 | # Can we determine a hacked account by the type of outgoing 5 | # Note: /var/log/mailog* is generally owned by root only 6 | 7 | use Data::Dumper qw(Dumper); 8 | 9 | %sender_list = (); #ip list 10 | %fsender_list = (); #failed ip list 11 | $audit_log = 0; #todays logging 12 | 13 | chdir "/var/log"; 14 | 15 | for (glob 'maillog*') { 16 | 17 | open (IN, sprintf("zcat -f %s |", $_)) 18 | or die("Can't open pipe from command 'cat $filename' : $!\n"); 19 | 20 | my($PFID) = 0; 21 | while () 22 | { 23 | if (m#postfix/smtpd#i) 24 | { 25 | if (m#client=#) 26 | { 27 | ($PFID) = m#]:\s*(.*):#; 28 | #print "id is $PFID\n"; 29 | } 30 | elsif (m#<>:#) 31 | { 32 | my($recipient) = m#to=<(.*)>\s*proto#; 33 | ++$fsender_list{$recipient} if ($recipient ne ""); 34 | } 35 | elsif (m#filter|127.0.0.1# && $PFID) 36 | { 37 | my($sender,$recipient) = m#from=<(.*)>\s*to=<(.*)>\s*proto#; 38 | #print " sender is $sender, recipient is $recipient\n"; 39 | #print $_; 40 | 41 | ++$sender_list{$PFID}{$sender} if ($sender ne ""); 42 | ++$sender_list{$PFID}{'bounce'} if ($sender eq ""); 43 | #$sender_list{$PFID}{'recipient'} = $recipient . ' ' . $sender_list{$PFID}{'recipient'} if ($recipient ne ""); 44 | } 45 | } 46 | } 47 | close (IN); 48 | } 49 | 50 | 51 | 52 | #debug 53 | #print Dumper \%sender_list; 54 | #print Dumper \%fsender_list; 55 | 56 | for $PFID (sort keys %sender_list ) 57 | { 58 | 59 | # print "\n",$sender_list{$PFID}{sender},"\n"; 60 | 61 | 62 | for $user (keys %{$sender_list{$PFID}}) 63 | { 64 | if (exists $sender_list{$PFID}{$user}) 65 | { 66 | # See cont of how many times 67 | if ($sender_list{$PFID}{$user} > 20) 68 | { 69 | printf ("%s Total [%4d] - %15s ",$PFID, $sender_list{$PFID}{$user},$user); 70 | printf " Bounces [%4d]", $sender_list{$PFID}{'bounce'} if $sender_list{$PFID}{'bounce'}; 71 | #print $_, "\n", for split ' ', $sender_list{$PFID}{'recipient'}; 72 | 73 | printf ("\n"); 74 | } 75 | } 76 | 77 | } 78 | } 79 | 80 | # Abnormal bounces for users 81 | for $user (sort keys %fsender_list ) 82 | { 83 | if (exists $fsender_list{$user}) 84 | { 85 | # See cont of how many times 86 | if ($fsender_list{$user} > 10) 87 | { 88 | printf ("Total Bounces [%4d] for %s\n", $fsender_list{$user},$user); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/check_rejected_spam.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # 4 | # Zimbra Assumptions: 5 | # Amavis at level 3 logging to see spam_scan lines in /var/log/zimbra.log to parse: 6 | # % zmprov ms `zmhostname` zimbraAmavisLogLevel 3 7 | # % zmantispamctl restart 8 | # 9 | # %%% not completed 10 | # Bugs: amavis is threaded and the logs contain multiple row records. 11 | # This script needs the parser to hold the data before counting. 12 | 13 | use Data::Dumper qw(Dumper); 14 | use Getopt::Long; 15 | 16 | %Email_list = (); #ip list 17 | %SA_Rules_list = (); #failed ip list 18 | $audit_log = 0; #todays logging 19 | 20 | sub usage { 21 | 22 | print <<"END"; 23 | usage: % check_spam.pl 24 | [--user=] 25 | [--ham|h ] 26 | [--spam|s ] 27 | [--discard|d ] 28 | [--rules|r ] 29 | [--option|o ] 30 | requires one of 31 | --ham | --spam | --discard 32 | where 33 | --ham will display only ham 34 | --spam will display only spam 35 | --discard will display not delivered email due to scoring 36 | --rules DO NOT display SA rules that fired 37 | --user will display only email destined for that user 38 | END 39 | exit 0; 40 | } 41 | 42 | #defaults 43 | my $srchuser = '@'; 44 | my $ham = 0; 45 | my $spam = 0; 46 | my $discard = 0; 47 | my $rules = 0; 48 | my $help = 0; 49 | my $dcount=0; 50 | my $scount=0; 51 | my $hcount=0; 52 | my $tcount=0; 53 | 54 | &GetOptions( "user=s" => \$srchuser, 55 | "ham" => \$ham, # display ham 56 | "discard" => \$discard, # display discarded not delivered email 57 | "rules" => \$rules, # display SA rules 58 | "spam" => \$spam, # display spam 59 | "options" => \$help); 60 | 61 | print "user is $srchuser rules[$rules] ham[$ham] spam[$spam] discard[$discard]\n"; 62 | my $nodisplayoptions=$ham + $spam + $discard; 63 | usage() if($help || !$nodisplayoptions); 64 | 65 | chdir "/var/log"; 66 | 67 | #for (glob 'zimbra.log*') { 68 | for (glob 'zimbra.log') { 69 | 70 | # audit.log is always todays stuff 71 | #print "***** Opening file $_","\n"; 72 | if ($_ eq 'zimbra.log') 73 | { 74 | $audit_log = 1; 75 | open (IN, sprintf("cat %s |", $_)) 76 | or die("Can't open pipe from command 'zcat $filename' : $!\n"); 77 | } 78 | else 79 | { 80 | $audit_log = 0; 81 | open (IN, sprintf("zcat %s |", $_)) 82 | or die("Can't open pipe from command 'zcat $filename' : $!\n"); 83 | } 84 | 85 | my $score=0; 86 | my $tests=""; 87 | my $flag=0; 88 | 89 | while () 90 | { 91 | # Available when in level 3 logging 92 | if (m#spam_scan#) 93 | { 94 | #print $_; 95 | ($score,$tests) = m#\s+score=(-?\d+\.?\d*).*tests=\[(.*)\]\s*#i; 96 | #print " - score is $score, tests is $tests \n"; 97 | 98 | # %%% spam_scan can be consequtive given this is multi-threaded writes from the amavisd's. 99 | # resulting in lost records. 100 | #if ($flag) {print " - score is $score, tests is $tests \n";} 101 | $flag=1; 102 | 103 | } 104 | # Always available 105 | # Discarded spam 106 | elsif (m#DiscardedInbound# && ($flag == 1) && (m#Blocked#)) 107 | { 108 | #print " - score is $score, tests is $tests \n"; 109 | my($from,$to,$hits,$size) = m#[^<]+<([^>]+)>[^<]+<([^>]+)\>.*Hits:\s*(\d+\.?\d*),\s*size:\s+(.*)$#i; 110 | 111 | #by user 112 | next if(index($to,$srchuser) == -1); 113 | next if (!$discard); 114 | 115 | # Sanity check for working on same record 116 | if ($hits != $score) { next; } 117 | 118 | printf ("Score [%6s] To: %s From: %s\n", $score, $to, $from); 119 | printf (" %s\n\n", $tests) if (!$rules); 120 | 121 | # reset, and look for next spam_scan line 122 | $score=0; 123 | $tests=""; 124 | $flag=0; 125 | $dcount++; 126 | } 127 | # Ham 128 | elsif (m#spam-tag# && ($flag == 1) && (m#No#)) 129 | { 130 | #print " - score is $score, tests is $tests \n"; 131 | my($from,$to,$hits) = m#spam-tag,\s+\<+([^>]+)\>+\s+-\>\s+\<+([^>]+)\>+,\s+No,\s+score=(-?\d+\.?\d*)\s+.*$#i; 132 | 133 | #by user 134 | next if(index($to,$srchuser) == -1); 135 | next if (!$ham); 136 | 137 | # Sanity check for working on same record 138 | if ($hits != $score) { next; } 139 | 140 | #print $_; 141 | 142 | printf ("Score [%6s] To: %s From: %s\n", $score, $to, $from); 143 | printf (" %s\n\n", $tests) if (!$rules); 144 | 145 | # reset, and look for next spam_scan line 146 | $score=0; 147 | $tests=""; 148 | $flag=0; 149 | $hcount++; 150 | } 151 | # Spam but not discarded 152 | elsif (m#spam-tag# && ($flag == 1) && (m#Yes#)) 153 | { 154 | #print " - score is $score, tests is $tests \n"; 155 | my($from,$to,$hits) = m#spam-tag,\s+\<+([^>]+)\>+\s+-\>\s+\<+([^>]+)\>+,\s+Yes,\s+score=(-?\d+\.?\d*)\s+.*$#i; 156 | 157 | #by user 158 | next if(index($to,$srchuser) == -1); 159 | next if (!$spam); 160 | 161 | # Sanity check for working on same record 162 | if ($hits != $score) { next; } 163 | 164 | #print $_; 165 | 166 | printf ("Score [%6s] To: %s From: %s\n", $score, $to, $from); 167 | printf (" %s\n\n", $tests) if (!$rules); 168 | 169 | # reset, and look for next spam_scan line 170 | $score=0; 171 | $tests=""; 172 | $flag=0; 173 | $scount++; 174 | } 175 | } 176 | close (IN); 177 | 178 | } 179 | 180 | $tcount += $dcount if ($discard); 181 | $tcount += $scount if ($spam); 182 | $tcount += $hcount if ($ham); 183 | 184 | printf ("\nTotal counts: $tcount"); 185 | printf (" Discarded Email: $dcount") if ($discard); 186 | printf (" Spam Email: $scount") if ($spam); 187 | printf (" Ham Email: $hcount") if ($ham); 188 | printf ("\n"); 189 | -------------------------------------------------------------------------------- /src/check_sa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # usage: check_sa.sh somefile.txt 5 | # 6 | # output: somefile.txt.out 7 | # 8 | # Documentation: 9 | # 10 | # https://wiki.apache.org/spamassassin/DebugChannels for -D 'dns,check', etc 11 | # 12 | #spamassassin -D --lint --dbpath /opt/zimbra/data/amavisd/.spamassassin 2>&1 < 16b9792b67f-10 13 | #spamassassin -D --lint --dbpath /opt/zimbra/data/amavisd/.spamassassin < 16b9792b67f-10 14 | #spamassassin -D --dbpath /opt/zimbra/data/amavisd/.spamassassin 2>&1 < 16b9792b67f-10 15 | #spamassassin -D -L --dbpath /opt/zimbra/data/amavisd/.spamassassin 2>&1 < 16b9792b67f-10 16 | #spamassassin -D -L --dbpath /opt/zimbra/data/amavisd/.spamassassin < 16b9792b67f-10 >/dev/null 17 | #spamassassin -Ddns -L --dbpath /opt/zimbra/data/amavisd/.spamassassin < 16b9792b67f-10 >/dev/null 18 | #spamassassin -D dns,bayes,check < 16b9792b67f-10 >/dev/null 19 | #spamassassin -D dns,bayes,check < 16b9792b67f-10 >/dev/null 20 | 21 | . /opt/zimbra/.bashrc 22 | 23 | #export PERL5LIB=/opt/zimbra/common/lib/perl5/x86_64-linux-thread-multi:/opt/zimbra/common/lib/perl5 24 | #export PERLLIB=/opt/zimbra/common/lib/perl5/x86_64-linux-thread-multi:/opt/zimbra/common/lib/perl5 25 | 26 | usage() { 27 | echo " 28 | 29 | usage: 30 | $ check_sa.sh file.txt 31 | $ check_sa.sh -n file.txt 32 | $ check_sa.sh -flags DNS,check file.txt 33 | $ check_sa.sh --log 34 | $ check_sa.sh --lint 35 | 36 | output: file.txt.out 37 | 38 | Options to run spamassassin in Debug Mode 39 | 40 | --flags DNS,bayes,check 41 | --lint 42 | --net (do network tests - remove -L option) 43 | --log 44 | --help 45 | 46 | see: https://wiki.apache.org/spamassassin/DebugChannels for -D 'dns,check' 47 | 48 | " 49 | } 50 | 51 | 52 | # default is local tests only 53 | nflags="-L" 54 | 55 | # 56 | args=$(getopt -l "log,flags:,help,lint,net" -o "f:hln" -- "$@") 57 | 58 | eval set -- "$args" 59 | 60 | while [ $# -ge 1 ]; do 61 | case "$1" in 62 | --) 63 | # No more options left. 64 | shift 65 | break 66 | ;; 67 | -f|--flags) 68 | flags="$2" 69 | shift 70 | ;; 71 | -n|--net) 72 | nflags="" 73 | ;; 74 | -l|--lint) 75 | spamassassin --lint 76 | exit 0 77 | ;; 78 | --log) 79 | grep "SA check" /var/log/zimbra.log | awk '{printf "Time:%s %d ms]\n", $10,$11}' 80 | exit 0 81 | ;; 82 | -h|--help) 83 | usage 84 | exit 0 85 | ;; 86 | esac 87 | 88 | shift 89 | done 90 | 91 | #echo "flags: $flags" 92 | #echo "remaining args: $*" 93 | file="$*" 94 | 95 | if [ ! -f $file ] || [ -z $file ]; then 96 | usage 97 | echo "****** ERROR: missing filename ******"; 98 | exit 1; 99 | fi 100 | 101 | echo "spamassassin -D $flags $nflags" ' < ' "$file" ' > ' "$file".out 102 | #spamassassin -D $flags $nflags < "$file" > /dev/null 2> "$file".out 103 | spamassassin -D $flags $nflags < "$file" 2>&1 >/dev/null | sed 's/__LOWER_E,//g;s/__E_LIKE_LETTER,//g' > "$file".out 104 | 105 | 106 | exit 0 107 | -------------------------------------------------------------------------------- /src/compare_next_zimbra_tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # usage: compare_next_zimbra_tag repository tag 4 | # compare_next_zimbra_tag zm-web-client 10.0.8 5 | # 6 | # Author: GPT 4.5 7 | # Human: Jim Dunphy Mar 25, 2025 8 | # 9 | # Algorithm: 10 | # 1. Given a Zimbra repository name and specific tag (e.g., zm-web-client 10.0.12). 11 | # 2. Find the next higher numeric tag in the same major.minor series (e.g., 10.0.13). 12 | # 3. If no next higher tag exists, report clearly "No change". 13 | # 4. Fetch only the two relevant tags (current and next highest). 14 | # 5. Compare tags directly: 15 | # a. If no differences exist, report "No change detected". 16 | # b. If differences exist, clearly display them. 17 | 18 | set -euo pipefail 19 | 20 | if [ $# -ne 2 ]; then 21 | echo "Usage: $0 " 22 | echo "Example: $0 zm-web-client 10.0.12" 23 | exit 1 24 | fi 25 | 26 | REPO_NAME="$1" 27 | CURRENT_TAG="$2" 28 | REMOTE_URL="https://github.com/Zimbra/${REPO_NAME}.git" 29 | TMP_DIR=$(mktemp -d) 30 | 31 | echo "Checking Zimbra repo: $REMOTE_URL" 32 | echo "Current tag specified: $CURRENT_TAG" 33 | 34 | cleanup() { 35 | rm -rf "$TMP_DIR" 36 | } 37 | trap cleanup EXIT 38 | 39 | git -C "$TMP_DIR" init -q 40 | git -C "$TMP_DIR" remote add origin "$REMOTE_URL" 41 | 42 | # Get major.minor prefix (e.g., "10.0") 43 | VERSION_PREFIX=$(echo "$CURRENT_TAG" | awk -F. '{print $1"."$2}') 44 | 45 | # Quickly list and numerically sort tags, correctly filtering higher patch numbers 46 | NEXT_TAG=$(git ls-remote --tags "$REMOTE_URL" "${VERSION_PREFIX}.*" \ 47 | | awk '{print $2}' \ 48 | | sed 's|refs/tags/||; s|\^{}||' \ 49 | | sort -t '.' -k1,1n -k2,2n -k3,3n \ 50 | | awk -v curr="$CURRENT_TAG" ' 51 | BEGIN { found=0 } 52 | { 53 | if(found) {print; exit} 54 | if($0 == curr) found=1 55 | }') 56 | 57 | if [ -z "$NEXT_TAG" ]; then 58 | echo "No higher tag found after '$CURRENT_TAG' in ${VERSION_PREFIX}.* series. No change." 59 | exit 0 60 | fi 61 | 62 | echo "Next higher tag found: $NEXT_TAG" 63 | 64 | # Fetch just these two tags quickly 65 | git -C "$TMP_DIR" fetch --quiet --depth 1 origin "refs/tags/$CURRENT_TAG:refs/tags/$CURRENT_TAG" 66 | git -C "$TMP_DIR" fetch --quiet --depth 1 origin "refs/tags/$NEXT_TAG:refs/tags/$NEXT_TAG" 67 | 68 | # Check for diffs 69 | if git -C "$TMP_DIR" diff --quiet "$CURRENT_TAG" "$NEXT_TAG"; then 70 | echo "No change detected between tags '$CURRENT_TAG' and '$NEXT_TAG'." 71 | else 72 | echo "Changes detected between tags '$CURRENT_TAG' and '$NEXT_TAG':" 73 | git -C "$TMP_DIR" --no-pager diff "$CURRENT_TAG" "$NEXT_TAG" 74 | fi 75 | 76 | -------------------------------------------------------------------------------- /src/compare_pimbra.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Author: GPT 4.5 5 | # Human: Jim Dunphy (Mar 24, 2025) 6 | # 7 | # usage: compare_pimbra.sh 8 | # 9 | # Caveat: assumes a repository was checked out for a verson of Zimbra. 10 | # ie) cd zm-web-client; compare_pimbra.sh 11 | # 12 | # 13 | # Algorithm: 14 | # 1. Determine the current local git tag (e.g., 10.0.12). 15 | # 2. Construct the exact remote tag by appending '-maldua' (e.g., 10.0.12-maldua). 16 | # 3. Check if this exact remote tag exists: 17 | # a. If yes, use it for comparison. 18 | # b. If no, find the highest available remote tag matching the same major.minor version 19 | # pattern with '-maldua' suffix (e.g., 10.0.13-maldua). 20 | # 4. Fetch only the chosen remote tag. 21 | # 5. Perform a diff between the local HEAD and the selected remote tag. 22 | # 23 | 24 | set -euo pipefail 25 | 26 | REMOTE_NAMESPACE="maldua-pimbra" 27 | REMOTE_TMP="tmp-maldua-remote" 28 | 29 | REPO_NAME=$(basename "$(git rev-parse --show-toplevel)") 30 | REMOTE_URL="git@github.com:${REMOTE_NAMESPACE}/${REPO_NAME}.git" 31 | 32 | echo "Local repository: $(git remote get-url origin)" 33 | echo "Remote repository: $REMOTE_URL" 34 | 35 | git remote remove "$REMOTE_TMP" &>/dev/null || true 36 | git remote add "$REMOTE_TMP" "$REMOTE_URL" 37 | 38 | # Determine local tag explicitly 39 | LOCAL_TAG=$(git describe --tags --exact-match 2>/dev/null || true) 40 | if [ -z "$LOCAL_TAG" ]; then 41 | echo "No local tag detected. Please checkout a tag first." 42 | git remote remove "$REMOTE_TMP" 43 | exit 1 44 | fi 45 | 46 | echo "Local tag detected: $LOCAL_TAG" 47 | 48 | # Extract major.minor from local tag (e.g., 10.0 from 10.0.12) 49 | VERSION_PREFIX=$(echo "$LOCAL_TAG" | awk -F. '{print $1 "." $2}') 50 | 51 | # First try exact match: LOCAL_TAG-maldua 52 | TARGET_REMOTE_TAG="${LOCAL_TAG}-maldua" 53 | echo "Checking remote for exact tag: $TARGET_REMOTE_TAG" 54 | 55 | if git ls-remote --exit-code --tags "$REMOTE_URL" "refs/tags/$TARGET_REMOTE_TAG" &>/dev/null; then 56 | echo "Exact remote tag found: $TARGET_REMOTE_TAG" 57 | else 58 | echo "Exact remote tag not found. Searching for highest '${VERSION_PREFIX}.*-maldua' tag." 59 | 60 | TARGET_REMOTE_TAG=$(git ls-remote --tags "$REMOTE_URL" "${VERSION_PREFIX}.*-maldua" \ 61 | | awk '{print $2}' \ 62 | | sed 's|refs/tags/||' \ 63 | | sort -Vr \ 64 | | head -n1) 65 | 66 | if [ -z "$TARGET_REMOTE_TAG" ]; then 67 | echo "No matching '${VERSION_PREFIX}.*-maldua' tags found remotely." 68 | git remote remove "$REMOTE_TMP" 69 | exit 1 70 | fi 71 | 72 | echo "Selected highest available tag: $TARGET_REMOTE_TAG" 73 | fi 74 | 75 | # Fetch ONLY this specific remote tag (fast) 76 | git fetch --quiet "$REMOTE_TMP" "refs/tags/$TARGET_REMOTE_TAG:refs/remotes/$REMOTE_TMP/$TARGET_REMOTE_TAG" 77 | 78 | echo "Running diff: Local ($LOCAL_TAG) vs. Remote ($TARGET_REMOTE_TAG)" 79 | 80 | # Check for common ancestor to prevent huge unrelated diffs 81 | if ! git merge-base --is-ancestor "HEAD" "$REMOTE_TMP/$TARGET_REMOTE_TAG" && \ 82 | ! git merge-base --is-ancestor "$REMOTE_TMP/$TARGET_REMOTE_TAG" "HEAD"; then 83 | echo "Warning: Tags don't share common history; diff may be large or unclear." 84 | fi 85 | 86 | # Diff without pager 87 | git --no-pager diff HEAD "$REMOTE_TMP/$TARGET_REMOTE_TAG" 88 | 89 | git remote remove "$REMOTE_TMP" 90 | 91 | -------------------------------------------------------------------------------- /src/compare_zimbra_updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Author: GPT 4.5 5 | # Human: Jim Dunphy (Mar 24, 2025) 6 | # 7 | # usage: compare_zimbra_updates.sh 8 | # 9 | # Caveat: assumes a repository was checked out for a verson of Zimbra. 10 | # ie) cd zm-web-client; compare_zimbra_updates.sh 11 | # 12 | # Algorithm: 13 | # 1. Determine the current local git tag (e.g., 10.0.13). 14 | # 2. Identify the highest available remote tag from Zimbra's GitHub repository 15 | # within the same major.minor series (e.g., 10.0.*). 16 | # 3. If the local tag is already the highest, report that no update is needed. 17 | # 4. Otherwise, fetch the highest remote tag explicitly. 18 | # 5. Compare local HEAD with the fetched remote tag: 19 | # a. If no differences, report "No change detected". 20 | # b. If differences exist, clearly display them. 21 | 22 | set -euo pipefail 23 | 24 | REPO_NAME=$(basename "$(git rev-parse --show-toplevel)") 25 | REMOTE_URL="https://github.com/Zimbra/${REPO_NAME}.git" 26 | REMOTE_TMP="tmp-zimbra-remote" 27 | 28 | echo "Local repository: $(git remote get-url origin)" 29 | echo "Checking Zimbra official repository: $REMOTE_URL" 30 | 31 | git remote remove "$REMOTE_TMP" &>/dev/null || true 32 | git remote add "$REMOTE_TMP" "$REMOTE_URL" 33 | 34 | LOCAL_TAG=$(git describe --tags --exact-match 2>/dev/null || true) 35 | 36 | if [ -z "$LOCAL_TAG" ]; then 37 | echo "Please checkout a tagged version first. No local tag detected." 38 | git remote remove "$REMOTE_TMP" 39 | exit 1 40 | fi 41 | 42 | echo "Local tag detected: $LOCAL_TAG" 43 | 44 | # Get major.minor from local tag (e.g., 10.0 from 10.0.13) 45 | VERSION_PREFIX=$(echo "$LOCAL_TAG" | awk -F. '{print $1"."$2}') 46 | 47 | # Find the highest available remote tag in the same major.minor series 48 | HIGHEST_REMOTE_TAG=$(git ls-remote --tags "$REMOTE_URL" "${VERSION_PREFIX}.*" \ 49 | | awk '{print $2}' \ 50 | | sed 's|refs/tags/||; s|\^{}||' \ 51 | | sort -Vr \ 52 | | head -n1) 53 | 54 | if [ -z "$HIGHEST_REMOTE_TAG" ]; then 55 | echo "No remote tags matching '${VERSION_PREFIX}.*' found." 56 | git remote remove "$REMOTE_TMP" 57 | exit 1 58 | fi 59 | 60 | echo "Highest remote tag found: $HIGHEST_REMOTE_TAG" 61 | 62 | # Check if local tag is already the latest 63 | if [ "$LOCAL_TAG" == "$HIGHEST_REMOTE_TAG" ]; then 64 | echo "You are already on the latest tag ($LOCAL_TAG). No updates." 65 | git remote remove "$REMOTE_TMP" 66 | exit 0 67 | fi 68 | 69 | # Fetch only the target remote tag quickly 70 | git fetch --quiet "$REMOTE_TMP" "refs/tags/$HIGHEST_REMOTE_TAG:refs/remotes/$REMOTE_TMP/$HIGHEST_REMOTE_TAG" 71 | 72 | # Check for differences between tags quickly 73 | if git diff --quiet "HEAD" "$REMOTE_TMP/$HIGHEST_REMOTE_TAG"; then 74 | echo "No change detected between local tag ($LOCAL_TAG) and remote tag ($HIGHEST_REMOTE_TAG)." 75 | else 76 | echo "Changes detected between local tag ($LOCAL_TAG) and remote tag ($HIGHEST_REMOTE_TAG):" 77 | git --no-pager diff "HEAD" "$REMOTE_TMP/$HIGHEST_REMOTE_TAG" 78 | fi 79 | 80 | git remote remove "$REMOTE_TMP" 81 | 82 | -------------------------------------------------------------------------------- /src/dump_trained_spam.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # usage: dump_trained_spam.sh 4 | # 5 | #Return-Path: bounces+10719421-d795-user=example@u10719421.wl172.sendgrid.net 6 | #X-Spam-Status: No, score=2.855 required=4.8 tests=[BAYES_95=3, 7 | # DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, 8 | # HEADER_FROM_DIFFERENT_DOMAINS=0.25, HTML_MESSAGE=0.001, 9 | # HTTP_IN_BODY=0.1, J_IMG_NO_EXTENS=0.5, J_RCVD_IN_HOSTKARMA_YEL=0.003, 10 | # J_URI_DOMAIN_BAD=0.1, MAILING_LIST_MULTI=-1, 11 | # RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001] 12 | # autolearn=no autolearn_force=no 13 | #From: Rose Smith 14 | #Subject: =?UTF-8?B?8J+GlQ==?= Website Development Proposal & Digital Marketing 15 | #To: "user@example.com" 16 | #----------------------------- 17 | #Return-Path: alexa@questmso.com 18 | #X-Spam-Status: No, score=1.728 required=4.8 tests=[BAYES_50=0.8, 19 | # DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, 20 | # HTML_MESSAGE=0.001, HTTP_IN_BODY=0.1, J_DOCTYPE_MISSING=0.5, 21 | # MIME_HTML_MOSTLY=0.428, RCVD_IN_DNSWL_NONE=-0.0001, 22 | # SPF_HELO_PASS=-0.001] autolearn=no autolearn_force=no 23 | #From: "QuestMSO LLC" 24 | #To: 25 | #Subject: Can You Help Me Please! 26 | #... 27 | #... 28 | # 29 | # 6/14/2019 - JAD 30 | 31 | # %%% if you only want to look at scores < 4 vs everything 32 | #for file in `grep X-Spam-Score /tmp/zmtrain*spam*/* | egrep ':\s+(-|0\.|1\.|2\.|3\.)' | awk -F: '{print }' | sort -u | awk -F: '{print $1}'` 33 | for file in `grep X-Spam-Score /tmp/zmtrain*spam*/* | awk -F: '{print }' | sort -u | awk -F: '{print $1}'` 34 | do 35 | cat $file | inspect_mail.pl 36 | echo "-----------------------------" 37 | done 38 | 39 | -------------------------------------------------------------------------------- /src/inspect_mail.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # 4 | # usage: cat file | inspect_mail.pl 5 | # 6 | #Return-Path: rosenjason789@gmail.com 7 | #X-Spam-Status: No, score=2.968 required=4.8 tests=[BAYES_80=2, 8 | # DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, 9 | # FREEMAIL_ENVFROM_END_DIGIT=0.25, FREEMAIL_FROM=0.001, 10 | # HTML_MESSAGE=0.001, J_DNSBL_MILTER_META=0.3, J_DOCTYPE_MISSING=0.5, 11 | # J_RCVD_IN_HOSTKARMA_YEL=0.003, RCVD_IN_DNSWL_NONE=-0.0001, 12 | # RCVD_IN_MSPIKE_H3=0.001, RCVD_IN_MSPIKE_WL=0.001, SPF_HELO_NONE=0.001, 13 | # T_GB_FREEMAIL_DISPTO=0.01] autolearn=no autolearn_force=no 14 | #From: "Jason Rosen" 15 | #To: 16 | #Subject: Health and Safety Professionals Email List 17 | # 18 | # 6/14/2019 - JAD 19 | 20 | use strict; 21 | use warnings; 22 | 23 | my $SPAM = ""; 24 | my $flag = 0; 25 | 26 | while () { 27 | 28 | my ($line) = $_; 29 | # chomp; 30 | # my ($line) = split; 31 | 32 | #print $line; 33 | print $line if ($line =~ /Return-Path:/); 34 | print $line if ($line =~ /^To:/); 35 | print $line if ($line =~ /^Subject:/); 36 | print $line if ($line =~ /^From:/); 37 | $flag = 1 if ($line =~ /^X-Spam-Status:/); 38 | if ($flag) 39 | { 40 | $SPAM = $SPAM . $line; 41 | } 42 | if ($line =~ /autolearn_force/) 43 | { 44 | print $SPAM if ($line =~ /autolearn_force/); 45 | $SPAM = ""; 46 | $flag = 0; 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/ip_manager.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # 4 | # Author: ChatGBT4 3/30/2023 5 | # Human: J Dunphy 6 | # 7 | # Use: Track ip addresses with a date count. 8 | # ip addresses can then be exported to a file which can be used 9 | # for milters, ipsets, etc. 10 | # 11 | # Purpose: We have a blacklist that tracks ip's attempting delivery to spam bait email addresses. 12 | # Those ip's are added to a list that a milter will then tag subsequent connections from matching 13 | # ip's with a header that our spam engines can later use in their scoring. 14 | # 15 | # This program allows us to choose how many years or days we want this list to be. 16 | # 17 | # 18 | 19 | use strict; 20 | use warnings; 21 | use DBI; 22 | use Getopt::Long; 23 | 24 | my $db_file = "ip_addresses.db"; 25 | 26 | my ($import_file, $add_ip, $export_file, $newer_than, $help); 27 | 28 | GetOptions( 29 | "import=s" => \$import_file, 30 | "add=s" => \$add_ip, 31 | "export=s" => \$export_file, 32 | "newer=s" => \$newer_than, 33 | "help" => \$help, 34 | ) or die("Error in command line arguments\n"); 35 | 36 | sub usage { 37 | print "Usage: $0 [options]\n"; 38 | print "Options:\n"; 39 | print " --import Import a file containing a list of IP addresses (1 per line)\n"; 40 | print " --add Add a single IP address to the list\n"; 41 | print " --export Export IP addresses that are newer than specified duration to a file (default: 10 days)\n"; 42 | print " --newer Duration in seconds, minutes, hours, days, weeks, months, or years (e.g. '10 days', '2 weeks')\n"; 43 | print " --help Display this help message\n"; 44 | exit; 45 | } 46 | 47 | usage() if $help; 48 | 49 | my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file", "", "", { RaiseError => 1, AutoCommit => 1 }); 50 | 51 | $dbh->do("CREATE TABLE IF NOT EXISTS ip_addresses (ip TEXT PRIMARY KEY, timestamp DATETIME DEFAULT (datetime('now')));"); 52 | 53 | sub import_ips { 54 | open my $fh, '<', $import_file or die "Cannot open $import_file: $!"; 55 | while (my $ip = <$fh>) { 56 | chomp $ip; 57 | eval { $dbh->do("INSERT OR REPLACE INTO ip_addresses (ip) VALUES (?)", undef, $ip) }; 58 | } 59 | close $fh; 60 | } 61 | 62 | sub add_ip_address { 63 | eval { $dbh->do("INSERT OR REPLACE INTO ip_addresses (ip) VALUES (?)", undef, $add_ip) }; 64 | } 65 | 66 | sub export_ips { 67 | $newer_than = '10 days' unless defined $newer_than; 68 | my $duration = $newer_than =~ /^\d/ ? "+$newer_than" : $newer_than; 69 | my $interval = "strftime('%s', 'now') - strftime('%s', timestamp)"; 70 | my $sth = $dbh->prepare("SELECT ip FROM ip_addresses WHERE $interval <= ?"); 71 | $sth->execute($duration); 72 | open my $fh, '>', $export_file or die "Cannot open $export_file: $!"; 73 | while (my ($ip) = $sth->fetchrow_array()) { 74 | print $fh "$ip\n"; 75 | } 76 | close $fh; 77 | } 78 | 79 | import_ips() if $import_file; 80 | add_ip_address() if $add_ip; 81 | export_ips() if $export_file; 82 | 83 | $dbh->disconnect; 84 | 85 | -------------------------------------------------------------------------------- /src/move-file-repository.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Learning what what is possible with Git 4 | # Say you have a repository like: ZimbraScripts/src 5 | # * In src is a bunch of files. You would like to tag one of these files but tags are at the repository level and not file. What to do? 6 | # 7 | # Solution: Write another script called: move-file-repository.sh that will take 2 arguments. 8 | # * the name of the repository (ZibmraScripts in this example) 9 | # * the name of the file you want to have its own repository (build_zimbra.sh) in this case. 10 | # After this script runs, you have a repository named: build_zimbra with a file inside called build_zimbra.sh 11 | # and all the commit history is preserved in addition to collapsing the src directory and removing the other 12 | # scripts that were part of the original repository. 13 | # 14 | # Example: 15 | # % mkdir junk 16 | # % cp -r ZimbraScripts junk/ 17 | # % cd junk 18 | # % ../git-commands/move-file-repository.sh ZimbraScripts src/build_zimbra.sh 19 | # % ls build_zimbra 20 | # build_zimbra.sh 21 | # 22 | # Apr 23, 2024 - JDunphy 23 | 24 | # Define variables 25 | REPO_URL="$1" 26 | FILE_PATH="$2" 27 | FILE_NAME=$(basename -- "$FILE_PATH") 28 | REPO_NAME="${FILE_NAME%.*}" # Remove file extension 29 | 30 | # Clone the repository 31 | git clone "$REPO_URL" "$REPO_NAME" 32 | cd "$REPO_NAME" 33 | 34 | # Filter the repository to only include the specified file 35 | git filter-repo --path "$FILE_PATH" --force 36 | 37 | # Move the file to the root directory and update the path in the history 38 | git filter-repo --path-rename "${FILE_PATH}:${FILE_NAME}" 39 | 40 | # Remove the original remote and add a new one if needed 41 | git remote remove origin 42 | # git remote add origin 43 | 44 | # Commit the path change if needed 45 | git add . 46 | git commit -m "Move ${FILE_NAME} to root directory and update paths in history" 47 | 48 | # Clean up and reduce the repository size 49 | git reflog expire --expire=now --all 50 | git gc --prune=now --aggressive 51 | 52 | echo "Repository for ${FILE_NAME} has been created as ${REPO_NAME}, with all paths updated." 53 | 54 | # End of the script 55 | 56 | -------------------------------------------------------------------------------- /src/show-me-the-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # March 13, 2024: 5 | # Author: GPT-4+ and directed by JDunphy 6 | # 7 | # Demonstration of an inside joke showing the insane process for FOSS builds for each patch release of 10 8 | # Provided they stay consistent, it does work however. 9 | # 10 | # Ref: https://forums.zimbra.org/viewtopic.php?t=72627 11 | # 12 | # 13 | # usage: Build me zimbra based on tags and not latest checked in. 14 | # 15 | # % show_me_the_branch.sh 7 #will build 10.0.7 16 | # % show_me_the_branch.sh #will build 10.0.7 17 | # % show_me_the_branch.sh 8 #will build 10.0.8 18 | # 19 | # Description: This will checkout the correct branch and generate a shell script to be used to compile Zimbra 20 | # which will generate a tarball of the release you attempted to build 21 | # 22 | # Pre-Requisites: You have a build envionment for supported versions of Zimbra. 23 | # 24 | # Simpleset way is to use Ian's zimbra-build-scripts 25 | # 26 | # % mkdir build-zimbra 27 | # % cd build-zimbra 28 | # % git clone https://github.com/ianw1974/zimbra-build-scripts 29 | # % zimbra-build-scripts/zimbra-build-helper.sh --install-deps 30 | # 31 | # ************ READ THIS ***************** 32 | # Caveat: This script leaves a checked out branch of zm_build which must be removed prior to 33 | # running this script again. 34 | # 35 | 36 | VERSION=0.0 # there will be no other version ;-) 37 | 38 | # Define the base repository 39 | REPO_URL="git@github.com:Zimbra/zm-build.git" 40 | BASE_VERSION="10.0" 41 | 42 | # Use the first command line argument as the latest tag version; default to 7 if not provided 43 | LATEST_TAG_VERSION=${1:-7} 44 | 45 | TAG_FOUND=false 46 | 47 | # Function to clone the repository for a specific version 48 | clone_repo() { 49 | git clone --depth 1 --branch "$BASE_VERSION.$1" $REPO_URL 50 | if [ $? -eq 0 ]; then 51 | TAG_FOUND=true 52 | echo "Successfully cloned $REPO_URL with tag $BASE_VERSION.$1." 53 | return 0 54 | else 55 | echo "Failed to clone $REPO_URL with tag $BASE_VERSION.$1." 56 | return 1 57 | fi 58 | } 59 | 60 | # Try cloning the repo from the latest tag version down to 0 61 | for (( version=$LATEST_TAG_VERSION; version>=0; version-- )); do 62 | if clone_repo $version; then 63 | break 64 | fi 65 | done 66 | 67 | # Check if a tag was found and cloned successfully 68 | if [ "$TAG_FOUND" = false ]; then 69 | echo "Unable to clone any tags from $REPO_URL." 70 | exit 1 71 | fi 72 | 73 | # Populate TAGS with all versions from the successful version down to 0 74 | for (( version=$LATEST_TAG_VERSION; version>=0; version-- )); do 75 | if [ $version -eq 0 ]; then 76 | # Add special cases for version 0 77 | TAGS+=("$BASE_VERSION.0-GA") 78 | TAGS+=("$BASE_VERSION.0") 79 | else 80 | TAGS+=("$BASE_VERSION.$version") 81 | fi 82 | done 83 | 84 | # Change directory to zm-build 85 | cd zm-build 86 | 87 | # Convert TAGS array to a comma-separated list for the build command 88 | TAGS_STRING=$(IFS=,; echo "${TAGS[*]}") 89 | 90 | echo "_____________ Zimbra build script for Release: $BASE_VERSION.$LATEST_TAG_VERSION ________________" 91 | 92 | cat << _END_OF_TEXT_ 93 | #!/bin/sh 94 | 95 | export PATH=/usr/local/bin:/usr/sbin:/usr/bin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/home/jad/bin:/usr/sbin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin 96 | 97 | cd zm-build 98 | 99 | # Build the source tree with the specified parameters 100 | ENV_CACHE_CLEAR_FLAG=true ./build.pl --ant-options -DskipTests=true --git-default-tag="$TAGS_STRING" --build-release-no="$BASE_VERSION.$LATEST_TAG_VERSION" --build-type=FOSS --build-release=DAFFODIL --build-release-candidate=GA --build-thirdparty-server=files.zimbra.com --no-interactive 101 | 102 | _END_OF_TEXT_ 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/show-mobile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Show users which have mobile syunc activated 4 | # 5 | # run as root 6 | # 7 | # usage: ./show-mobile.sh 8 | # +----------------+------------+---------------------+-------------+-------+------+ 9 | # | comment | mailbox_id | device_id | device_type | model | os | 10 | # +----------------+------------+---------------------+-------------+-------+------+ 11 | # | jd@example.com | 3 | android946699536255 | Android | NULL | NULL | 12 | # +----------------+------------+---------------------+-------------+-------+------+ 13 | # 14 | 15 | su - zimbra -c "mysql -e 'select mb.comment, md.mailbox_id, md.device_id, md.device_type, md.model, md.os from zimbra.mobile_devices md INNER JOIN zimbra.mailbox mb ON md.mailbox_id = mb.id ;'" 16 | 17 | -------------------------------------------------------------------------------- /src/show-user-counts-mysql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Author: ChatGBT-4 5 | # Human: Jim Dunphy 6 | # License (ISC): It's yours. Enjoy 7 | # 4/15/2023 8 | # 9 | # usage: show-user-counts.sh # needs to run as root user 10 | # 11 | #User Total Messages Total Folders Total Contacts 12 | #-------------------------------------------------------------------------------------------------------------- 13 | #user1@xxxxxxxxxxxxxx.com 0 17 0 14 | #user2@xxxxxxxxxxxxxx.com 0 16 1 15 | #user3@xxxxxxxxxxxxxx.com 19014 115 2314 16 | #... 17 | #user4@xxxxxxxxxxxxxx.com 19014 115 2314 18 | # 19 | # 20 | # Folder (type = 1): Represents a folder in the mailbox. 21 | # Tag (type = 2): Represents a tag that can be associated with items in the mailbox. 22 | # Conversation (type = 3): Represents a conversation, which is a group of related email messages. 23 | # Message (type = 5): Represents an individual email message. 24 | # Contact (type = 6): Represents a contact in the user's address book. 25 | # Document (type = 7): Represents a document saved in the user's mailbox. 26 | # Appointment (type = 11): Represents a calendar appointment. 27 | # Task (type = 13): Represents a task in the user's task list. 28 | # Wiki (type = 14): Represents a wiki page in the user's mailbox. 29 | # Chat (type = 15): Represents a chat message in the user's mailbox. 30 | # 31 | # These are the most common types you may encounter when working with Zimbra's MySQL database. The 32 | # type values are used to identify the specific kind of item in the mail_item table. 33 | # 34 | 35 | # need to run as root to make su to zimbra transparent 36 | ID=`id -u` 37 | if [ "x$ID" != "x0" ]; then 38 | echo "Run as root!" 39 | exit 1 40 | fi 41 | 42 | # Define a command string to be executed as the zimbra user 43 | commands=$(cat < 5 | # License (ISC): It's yours. Enjoy 6 | # Date: 10/28/2022 7 | # 8 | # usage: switch_mail.sh --[stop|start] [zimbra|carbonio] 9 | 10 | # 11 | # 11/1/2022 - jad @ 12 | 13 | # 14 | # Purpose: 15 | # script to freeze zimbra/cabonio installed running on a host 16 | # Allows one to stop them from running and easily switch between them 17 | # or run non of them until you need to test/verify them. 18 | # 19 | 20 | usage() { 21 | echo " 22 | usage: 23 | 24 | freeze - moves cron aside and stops any systemctl services 25 | unfreeze - moves everything back 26 | 27 | $ switch_mail.sh --stop zimbra 28 | $ switch_mail.sh --start carbonio 29 | $ switch_mail.sh --stop zimbra 30 | $ switch_mail.sh --start carbonio 31 | $ switch_mail.sh --flip 32 | 33 | Can only have one active at a time 34 | " 35 | } 36 | 37 | doCarbonio() { 38 | cmd="$1" # stop|start|disable|enable 39 | 40 | systemctl $cmd carbonio-docs-connector 41 | systemctl $cmd carbonio-docs-connector-sidecar.service 42 | systemctl $cmd carbonio-docs-connector.service 43 | systemctl $cmd carbonio-files-db-sidecar.service 44 | systemctl $cmd carbonio-files-sidecar.service 45 | systemctl $cmd carbonio-files.service 46 | systemctl $cmd carbonio-mailbox-sidecar.service 47 | systemctl $cmd carbonio-mta-sidecar.service 48 | systemctl $cmd carbonio-proxy-sidecar.service 49 | systemctl $cmd carbonio-storages-sidecar.service 50 | systemctl $cmd carbonio-storages.service 51 | systemctl $cmd carbonio-user-management-sidecar.service 52 | systemctl $cmd carbonio-user-management.service 53 | systemctl $cmd carbonio-docs-editor.service 54 | systemctl $cmd service-discover.service 55 | 56 | } 57 | 58 | 59 | freeze=0 #default do nothing 60 | 61 | args=$(getopt -l "help,stop,start" -o "sg" -- "$@") 62 | eval set -- "$args" 63 | 64 | while [ $# -ge 1 ]; do 65 | case "$1" in 66 | --) 67 | # No more options left. 68 | shift 69 | break 70 | ;; 71 | -s|--stop) 72 | freeze=1 73 | ;; 74 | -g|--start) 75 | freeze=0 76 | ;; 77 | -h|--help) 78 | usage 79 | exit 0 80 | ;; 81 | esac 82 | 83 | shift 84 | done 85 | 86 | mail="$*" 87 | 88 | #echo "freeze: $freeze" 89 | #echo "remaining args: $*" 90 | #echo "mail is [$mail]" 91 | 92 | case "$mail" in 93 | 'carbonio') 94 | echo "doing carbonio actions" 95 | if [ $freeze == 1 ]; then 96 | echo "****** zmcontrol stop" 97 | su - zextras -c "zmcontrol stop" 98 | mv /var/spool/cron/zextras /var/spool/cron/zextras- 99 | doCarbonio stop 100 | doCarbonio disable 101 | chkconfig --level 2345 carbonio off 102 | else 103 | echo "****** zmcontrol start" 104 | su - zextras -c "zmcontrol start" 105 | mv /var/spool/cron/zextras- /var/spool/cron/zextras 106 | doCarbonio enable 107 | doCarbonio start 108 | chkconfig --level 2345 carbonio on 109 | fi 110 | ;; 111 | 'zimbra') 112 | echo "doing zimbra actions" 113 | if [ $freeze == 1 ]; then 114 | echo "****** zmcontrol stop" 115 | su - zimbra -c "zmcontrol stop" 116 | mv /var/spool/cron/zimbra /var/spool/cron/zimbra- 117 | chkconfig --level 2345 zimbra off 118 | else 119 | echo "****** zmcontrol start" 120 | su - zimbra -c "zmcontrol start" 121 | mv /var/spool/cron/zimbra- /var/spool/cron/zimbra 122 | chkconfig --level 2345 zimbra on 123 | fi 124 | ;; 125 | *) 126 | usage 127 | exit 0 128 | ;; 129 | esac 130 | -------------------------------------------------------------------------------- /src/tag-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Learning what what is possible with Git 4 | # Say you have a repository like: ZimbraScripts/src 5 | # * In src is a bunch of files. You would like to tag one of these files but tags are at the repository level and not file. What to do? 6 | # 7 | # Solution: Write another script called: move-file-repository.sh that will take 2 arguments. 8 | # * the name of the repository (ZibmraScripts in this example) 9 | # * the name of the file you want to have its own repository (build_zimbra.sh) in this case. 10 | # After this script runs, you have a repository named: build_zimbra with a file inside called build_zimbra.sh 11 | # and all the commit history is preserved in addition to collapsing the src directory and removing the other 12 | # scripts that were part of the original repository. 13 | # 14 | # Example: 15 | # mkdir test; cd test; cp -r ../ZimbraScripts .; ../git-commands/move-file-repository.sh ZimbraScripts src/build_zimbra.sh 16 | # 17 | # Now to the taging problem. You have kept your own version variable in these programs but would like to have a corresponding 18 | # git tag associated with this. That is what this script will do. It can also handle holes if you decided to 19 | # not use tag 1.13 because it is bad luck or didn't have version in your scripot. ;-) 20 | # 21 | # check results by: git tag 22 | # 23 | # Apr 23, 2024 - JDunphy 24 | 25 | # Navigate to the repository directory 26 | #cd path/to/build_zimbra 27 | 28 | # Store the current branch 29 | current_branch=$(git rev-parse --abbrev-ref HEAD) 30 | 31 | # Iterate over each commit 32 | for commit_hash in $(git log --reverse --pretty=format:"%H"); do 33 | # Checkout each commit 34 | git checkout $commit_hash 35 | 36 | # Read the version number from build_zimbra.sh. At some point, we changed the name of the variable 37 | version=$(egrep '(Version=|scriptVersion=)' build_zimbra.sh | cut -d'=' -f2 | tr -d '[:space:]') 38 | 39 | # Check if version is empty and continue if so 40 | if [ -z "$version" ]; then 41 | continue 42 | fi 43 | 44 | # Check if the tag already exists to avoid errors and ensure continuity 45 | if git rev-parse "v$version" >/dev/null 2>&1; then 46 | echo "Tag v$version already exists. Skipping..." 47 | else 48 | # Tag the commit with the version 49 | git tag -a "v$version" -m "Version $version" 50 | fi 51 | done 52 | 53 | # Checkout the original branch 54 | git checkout $current_branch 55 | 56 | # Push all tags to remote, if desired 57 | # %%% eventually but not now 58 | # git push --tags 59 | 60 | echo "All commits have been tagged based on their scriptVersion." 61 | -------------------------------------------------------------------------------- /src/vi-email.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Description: 4 | # edit a list of files matching a pattern 5 | # usage: 6 | # vi-email.sh example.com 7 | # expected results: 8 | # Any file that contained example.com would be in the edit list 9 | # ^n for vi users to cycle through list 10 | 11 | email=$1 12 | 13 | vi `grep $email /tmp/zmtrain*/* | awk -F: '{print $1}' | sort -u` 14 | ls -l `grep $email /tmp/zmtrain*/* | awk -F: '{print $1}' | sort -u` 15 | -------------------------------------------------------------------------------- /src/zm-audit-log.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # Author: GPT4o and Claude 3.5 Sonnet 4 | # Human: Jim Dunphy 5 | # License (ISC): It's yours. Enjoy 6 | # 1/22/2025 7 | # 8 | # usage: zm-audit-log.pl 9 | # 10 | # % zm-audit-log.pl --help 11 | # Zimbra Audit Log Analyzer version 1.0.1 12 | # 13 | # Usage: zm-audit-log.pl [options] 14 | # Options: 15 | # --dir=DIR Specify log directory (default: /opt/zimbra/log) 16 | # --file=FILE Specify single log file (default: DIR/audit.log) 17 | # --all Process all audit.log* files in directory 18 | # --user=EMAIL Show details for specific user 19 | # --list List all users 20 | # --help Show this help message 21 | # --version Show version information 22 | # 23 | # Caveat: 24 | # Permission: /opt/zimbra/log/audit.log is owned by zimbra. Recommendation is to run this as the zimbra user or 25 | # add your account to the zimbra group. 26 | # Failures: Currently not checking for failures yet. 27 | # 28 | # Bug Fix: zimbraMailTreustedIP was assumed so adding this if oip doesn't exist: 29 | # my ($ip) = $line =~ /(oip|ip)=([^;]+)/ ? $2 : undef; # Capture either oip or ip 30 | # 31 | # Sample output: 32 | # 33 | # % zm-audit-log.pl --file=/tmp/myaudit.log 34 | #Processing /tmp/myaudit.log... 35 | #+--------------------------------+---------------------+-----------------------+--------------------------------------------------------------------+ 36 | #| Email | Last Seen | Auth Methods | IP Addresses | 37 | #+--------------------------------+---------------------+-----------------------+--------------------------------------------------------------------+ 38 | #| Dlastname@example.com | 2025-01-16 09:02:03 | WebClient | X.X.X.X | 39 | #| Flastname@example.com | 2025-01-21 14:30:41 | WebClient | 174.224.208.9, 174.224.211.89, 174.224.212.99, 174.239.114.245, | 40 | #| 174.239.121.80, X.X.X.X | 41 | #| Fname.Alastname@example.com | 2025-01-21 00:38:32 | POP3 | X.X.X.X | 42 | #| archive@example.net | 2025-01-20 23:38:54 | POP3 | X.X.X.X | 43 | #| ceo@example.com | 2025-01-21 00:08:54 | POP3 | X.X.X.X | 44 | #| dan.Blastname@example.com | 2025-01-20 19:21:35 | WebClient | X.X.X.X | 45 | #| jackie.Clastname@example.com | 2025-01-21 07:28:47 | WebClient | X.X.X.X | 46 | #| jad@example.com | 2025-01-21 15:26:24 | IMAP | X.X.X.X | 47 | #| jesse@example.com | 2025-01-21 22:53:44 | ActiveSync, WebClient | 172.56.100.202, 172.56.100.244, 172.56.100.68, 172.56.101.140, | 48 | #| 172.56.101.18, 172.56.101.190, 172.56.101.32, 172.56.101.58, | 49 | #| 172.56.101.88, 172.56.102.100, 172.56.102.106, 172.56.102.108, | 50 | #| 172.56.102.182, 172.56.102.188, 172.56.102.198, 172.56.102.254, | 51 | #| 172.56.103.108, 172.56.103.202, 172.56.103.236, 172.56.103.26, | 52 | #| 172.56.103.90, 172.56.98.102, 172.56.98.106, 172.56.98.126, | 53 | #| 172.56.98.163, 172.56.98.36, 172.56.98.45, 172.56.98.65, | 54 | #| 172.56.99.103, 172.56.99.127, 172.56.99.35, 174.211.96.19, | 55 | #| 35.137.195.0, X.X.X.X | 56 | #| michelle.Elastname@example.com | 2025-01-21 06:37:33 | WebClient | X.X.X.X | 57 | #| name@example.com | 2025-01-21 00:08:54 | POP3 | X.X.X.X | 58 | #+--------------------------------+---------------------+-----------------------+--------------------------------------------------------------------+ 59 | # 60 | # % zm-audit-log.pl --user=name@Dlastname@example.com 61 | # 62 | 63 | use strict; 64 | use warnings; 65 | use Data::Dumper; 66 | use Time::Piece; 67 | use Getopt::Long; 68 | use Term::ANSIColor; 69 | 70 | our $VERSION = "1.0.3"; # Version tracking 71 | 72 | # Command line options 73 | my $log_dir = '/opt/zimbra/log'; 74 | my $log_file = "$log_dir/audit.log"; 75 | my $target_user = ''; 76 | my $list_users = 0; 77 | my $help = 0; 78 | my $show_version = 0; 79 | my $process_all = 0; 80 | 81 | GetOptions( 82 | "file=s" => \$log_file, 83 | "dir=s" => \$log_dir, 84 | "user=s" => \$target_user, 85 | "list" => \$list_users, 86 | "help" => \$help, 87 | "version" => \$show_version, 88 | "all" => \$process_all 89 | ) or die "Error in command line arguments\n"; 90 | 91 | if ($show_version) { 92 | print "Zimbra Audit Log Analyzer version $VERSION\n"; 93 | exit; 94 | } 95 | 96 | if ($help) { 97 | print "Zimbra Audit Log Analyzer version $VERSION\n\n"; 98 | print "Usage: $0 [options]\n"; 99 | print "Options:\n"; 100 | print " --dir=DIR Specify log directory (default: /opt/zimbra/log)\n"; 101 | print " --file=FILE Specify single log file (default: DIR/audit.log)\n"; 102 | print " --all Process all audit.log* files in directory\n"; 103 | print " --user=EMAIL Show details for specific user\n"; 104 | print " --list List all users\n"; 105 | print " --help Show this help message\n"; 106 | print " --version Show version information\n"; 107 | exit; 108 | } 109 | 110 | # Data structures to store our analysis 111 | my %users; # Store user activity 112 | my %devices; # Store device information 113 | my %ip_tracking; # Track IP addresses 114 | my %auth_methods; # Track authentication methods 115 | 116 | # Get list of log files to process 117 | sub get_log_files { 118 | my $dir = shift; 119 | my @files; 120 | 121 | if ($process_all) { 122 | # Get all audit log files 123 | @files = glob("$dir/audit.log*"); 124 | print "Found ", scalar(@files), " audit log files to process.\n"; 125 | } else { 126 | @files = ($log_file); 127 | } 128 | 129 | return sort @files; 130 | } 131 | 132 | # Process a single log file 133 | sub process_log_file { 134 | my ($file) = @_; 135 | my $errors = 0; 136 | 137 | print "Processing $file...\n"; 138 | 139 | # Use zcat -f for both compressed and uncompressed files 140 | open(my $fh, '-|', "zcat -f $file 2>/dev/null") or do { 141 | warn "Cannot open $file: $!\n"; 142 | return 1; 143 | }; 144 | 145 | while (my $line = <$fh>) { 146 | chomp $line; 147 | 148 | # Skip system zimbra authentication lines 149 | next if $line =~ /account=zimbra;/; 150 | 151 | # Extract timestamp (handle case with rsyslog using imfile module also) 152 | my ($timestamp) = $line =~ /^(?:\S+\s+\d{1,2} \d{2}:\d{2}:\d{2} \S+ \S+:? )?(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+Z)?)/; 153 | next unless $timestamp; # Skip lines without timestamp 154 | 155 | # Process app-specific password authentications 156 | if ($line =~ /successfully logged in with app-specific password/) { 157 | my ($account) = $line =~ /account ([^ ]+) successfully/; 158 | my ($ip) = $line =~ /oip=([^;]+)/ ? $1 : $line =~ /ip=([^;]+)/ ? $1 : undef; # Prioritize oip 159 | 160 | next unless ($account && $ip); 161 | $users{$account}{app_specific}{last_seen} = $timestamp; 162 | $users{$account}{app_specific}{last_ip} = $ip; 163 | $users{$account}{ips}{$ip}{last_seen} = $timestamp; 164 | $users{$account}{ips}{$ip}{count}++; 165 | $auth_methods{$account}{app_specific}++; # This is the counter that needs to be updated 166 | } 167 | # Process ActiveSync authentications 168 | elsif ($line =~ /Microsoft-Server-ActiveSync/) { 169 | my ($user) = $line =~ /User=([^&]+)/; 170 | my ($device_id) = $line =~ /DeviceId=([^&]+)/; 171 | my ($device_type) = $line =~ /DeviceType=([^&]+)/; 172 | my ($ip) = $line =~ /oip=([^;]+)/ ? $1 : $line =~ /ip=([^;]+)/ ? $1 : undef; # Prioritize oip 173 | my ($account) = $line =~ /account=([^;]+)/; 174 | 175 | 176 | # Skip if we don't have all required fields 177 | next unless ($user && $device_id && $device_type && $ip && $account); 178 | 179 | # Create/update basic device info 180 | $users{$account}{active_sync}{last_seen} = $timestamp; 181 | $users{$account}{active_sync}{devices}{$device_id} = { 182 | device_type => $device_type, 183 | model => $device_type, 184 | last_ip => $ip, 185 | last_seen => $timestamp 186 | }; 187 | 188 | $devices{$device_id}{account} = $account; 189 | $devices{$device_id}{type} = $device_type; 190 | $devices{$device_id}{last_seen} = $timestamp; 191 | 192 | $users{$account}{ips}{$ip}{last_seen} = $timestamp; 193 | $users{$account}{ips}{$ip}{count}++; 194 | } 195 | # Process 2FA authentications 196 | elsif ($line =~ /two-factor auth successful/) { 197 | my ($account) = $line =~ /account=([^;]+)/ ? $1 : $line =~ /name=([^;]+)/ ? $1 : undef; 198 | next unless ($account && $account =~ /@/); 199 | 200 | $users{$account}{security}{auth_type} = "2FA"; 201 | $users{$account}{security}{last_2fa} = $timestamp; 202 | $auth_methods{$account}{two_factor_auth}++; 203 | } 204 | # Process trusted device authentications 205 | elsif ($line =~ /trusted device verified.*bypassing two-factor auth/) { 206 | my ($account) = $line =~ /account=([^;]+)/ ? $1 : $line =~ /name=([^;]+)/ ? $1 : undef; 207 | my ($ip) = $line =~ /oip=([^;]+)/ ? $1 : $line =~ /ip=([^;]+)/ ? $1 : undef; # Prioritize oip 208 | my ($ua) = $line =~ /ua=([^;]+)/; 209 | 210 | # Skip if we don't have required fields or if it's the zimbra account 211 | next unless ($account && $ip); 212 | next if $account eq 'zimbra'; 213 | next unless $account =~ /@/; # Skip invalid email addresses 214 | 215 | $users{$account}{security}{auth_type} = "Trusted Device"; 216 | $users{$account}{security}{last_trusted} = $timestamp; 217 | $auth_methods{$account}{trusted_device}++; 218 | 219 | 220 | $users{$account}{web_client}{last_seen} = $timestamp; 221 | $users{$account}{web_client}{last_ip} = $ip; 222 | $users{$account}{web_client}{user_agent} = $ua if $ua; 223 | 224 | $users{$account}{ips}{$ip}{last_seen} = $timestamp; 225 | $users{$account}{ips}{$ip}{count}++; 226 | } 227 | # Process web client authentications and batch requests 228 | elsif ($line =~ /ZimbraWebClient/ || $line =~ /BatchRequest/) { 229 | my ($account) = $line =~ /account=([^;]+)/ ? $1 : $line =~ /name=([^;]+)/ ? $1 : undef; 230 | my ($ua) = $line =~ /ua=([^;]+)/; 231 | 232 | # Use oip if available, otherwise fall back to ip 233 | # Can happen when zimbraMailTrustedIP isn't set for the proxy 234 | my ($ip) = $line =~ /oip=([^;]+)/ ? $1 : $line =~ /ip=([^;]+)/ ? $1 : undef; # Prioritize oip 235 | 236 | # Skip if we don't have required fields or if it's the zimbra account 237 | next unless ($account && $ip); 238 | next if $account eq 'zimbra'; 239 | next unless $account =~ /@/; # Skip invalid email addresses 240 | 241 | $users{$account}{web_client}{last_seen} = $timestamp; 242 | $users{$account}{web_client}{last_ip} = $ip; 243 | $users{$account}{web_client}{user_agent} = $ua if $ua; 244 | 245 | $users{$account}{ips}{$ip}{last_seen} = $timestamp; 246 | $users{$account}{ips}{$ip}{count}++; 247 | } 248 | # Process POP3/IMAP authentications 249 | elsif ($line =~ /protocol=(pop3|imap);/) { 250 | my ($account) = $line =~ /account=([^;]+)/; 251 | my ($ip) = $line =~ /oip=([^;]+)/ ? $1 : $line =~ /ip=([^;]+)/ ? $1 : undef; # Prioritize oip 252 | my ($protocol) = $line =~ /protocol=([^;]+)/; 253 | 254 | 255 | next unless ($account && $ip); 256 | next if $account eq 'zimbra'; 257 | next unless $account =~ /@/; # Skip invalid email addresses 258 | 259 | #%%% 260 | #print "ip [$ip] protocol [$protocol] time [$timestamp] [$line]\n"; exit; 261 | 262 | $users{$account}{"${protocol}_client"}{last_seen} = $timestamp; 263 | $users{$account}{"${protocol}_client"}{last_ip} = $ip; 264 | 265 | $users{$account}{ips}{$ip}{last_seen} = $timestamp; 266 | $users{$account}{ips}{$ip}{count}++; 267 | $auth_methods{$account}{$protocol}++; 268 | 269 | } 270 | } 271 | 272 | close($fh); 273 | return 0; 274 | } 275 | 276 | # Process log files 277 | my $total_errors = 0; 278 | foreach my $file (get_log_files($log_dir)) { 279 | $total_errors += process_log_file($file); 280 | } 281 | 282 | # Report any processing errors 283 | if ($total_errors > 0) { 284 | print "\nWarning: Encountered problems with $total_errors file(s)\n"; 285 | } 286 | 287 | # Default to --list if no specific action 288 | if (!$target_user && !$list_users) { 289 | $list_users = 1; 290 | } 291 | 292 | # List all users if requested 293 | if ($list_users) { 294 | my @rows; 295 | foreach my $user (sort keys %users) { 296 | my @methods; 297 | push @methods, "ActiveSync" if exists $users{$user}{active_sync}; 298 | push @methods, "WebClient" if exists $users{$user}{web_client}; 299 | push @methods, "AppSpecific" if exists $users{$user}{app_specific}; 300 | push @methods, "POP3" if exists $users{$user}{pop3_client}; 301 | push @methods, "IMAP" if exists $users{$user}{imap_client}; 302 | 303 | # Add 2FA status to methods if applicable 304 | if (exists $users{$user}{security}) { 305 | if (exists $users{$user}{security}{last_2fa}) { 306 | push @methods, "2FA"; 307 | } 308 | elsif (exists $users{$user}{security}{last_trusted}) { 309 | push @methods, "2FA (Trusted)"; 310 | } 311 | } 312 | 313 | my $last_seen = ""; 314 | foreach my $type (qw(active_sync web_client app_specific pop3_client imap_client)) { 315 | if (exists $users{$user}{$type} && exists $users{$user}{$type}{last_seen}) { 316 | $last_seen = $users{$user}{$type}{last_seen} 317 | if (!$last_seen || $users{$user}{$type}{last_seen} gt $last_seen); 318 | } 319 | } 320 | 321 | # Format the IP addresses, 4 per line 322 | my @ips = sort keys %{$users{$user}{ips}}; 323 | my $ips_per_line = 4; 324 | my $formatted_ips = ''; 325 | 326 | # Format first line 327 | for (my $i = 0; $i < @ips; $i++) { 328 | $formatted_ips .= $ips[$i]; 329 | if ($i < $#ips) { # If not the last IP 330 | $formatted_ips .= ", "; 331 | if (($i + 1) % $ips_per_line == 0) { 332 | $formatted_ips .= "\n"; # Just newline, no indentation here 333 | } 334 | } 335 | } 336 | 337 | push @rows, [ 338 | $user, 339 | $last_seen, 340 | join(", ", @methods), 341 | $formatted_ips 342 | ]; 343 | 344 | } 345 | print format_all_table(['Email', 'Last Seen', 'Auth Methods', 'IP Addresses'], \@rows); 346 | exit; 347 | } 348 | 349 | # Show details for specific user 350 | if ($target_user) { 351 | if (!exists $users{$target_user}) { 352 | print "No data found for user: $target_user\n"; 353 | exit 1; 354 | } 355 | 356 | print colored(['bold'], "\nUser Details: $target_user\n\n"); 357 | 358 | # Show POP3 access 359 | if (exists $users{$target_user}{pop3_client}) { 360 | print colored(['bold'], "POP3 Client Access:\n"); 361 | my @rows; 362 | my $pop3_info = $users{$target_user}{pop3_client}; 363 | push @rows, [ 364 | $pop3_info->{last_seen} // 'N/A', 365 | $pop3_info->{last_ip} // 'N/A', 366 | $pop3_info->{orig_ip} // 'N/A' 367 | ]; 368 | print format_table(['Last Seen', 'Last IP', 'Original IP'], \@rows); 369 | print "\n"; 370 | } 371 | 372 | # Show IMAP access 373 | if (exists $users{$target_user}{imap_client}) { 374 | print colored(['bold'], "IMAP Client Access:\n"); 375 | my @rows; 376 | my $imap_info = $users{$target_user}{imap_client}; 377 | push @rows, [ 378 | $imap_info->{last_seen} // 'N/A', 379 | $imap_info->{last_ip} // 'N/A', 380 | $imap_info->{orig_ip} // 'N/A' 381 | ]; 382 | print format_table(['Last Seen', 'Last IP', 'Original IP'], \@rows); 383 | print "\n"; 384 | } 385 | 386 | # Show devices 387 | if (exists $users{$target_user}{active_sync}) { 388 | print colored(['bold'], "ActiveSync Devices:\n"); 389 | my @rows; 390 | foreach my $device (sort keys %{$users{$target_user}{active_sync}{devices}}) { 391 | my $dev_info = $users{$target_user}{active_sync}{devices}{$device}; 392 | push @rows, [ 393 | $device, 394 | $dev_info->{model}, 395 | $dev_info->{last_seen}, 396 | $dev_info->{last_ip} 397 | ]; 398 | } 399 | print format_table(['Device ID', 'Device Model', 'Last Seen', 'Last IP'], \@rows); 400 | print "\n"; 401 | } 402 | 403 | # Show web client access 404 | if (exists $users{$target_user}{web_client}) { 405 | print colored(['bold'], "Web Client Access:\n"); 406 | my @rows; 407 | my $web_info = $users{$target_user}{web_client}; 408 | push @rows, [ 409 | $web_info->{last_seen} // 'N/A', 410 | $web_info->{last_ip} // 'N/A', 411 | $web_info->{user_agent} // 'N/A' 412 | ]; 413 | print format_table(['Last Seen', 'Last IP', 'User Agent'], \@rows); 414 | print "\n"; 415 | } 416 | 417 | # Show security info 418 | if (exists $users{$target_user}{security} || exists $auth_methods{$target_user}) { 419 | print colored(['bold'], "Security Info:\n"); 420 | my @rows; 421 | if (exists $users{$target_user}{security}) { 422 | if (exists $users{$target_user}{security}{last_2fa}) { 423 | push @rows, ["2FA", $users{$target_user}{security}{last_2fa}, "Last successful 2FA login"]; 424 | } 425 | if (exists $users{$target_user}{security}{last_trusted}) { 426 | push @rows, ["Trusted Device", $users{$target_user}{security}{last_trusted}, "Last trusted device login"]; 427 | } 428 | } 429 | my $two_fa_count = $auth_methods{$target_user}{two_factor_auth} // 0; 430 | my $trusted_count = $auth_methods{$target_user}{trusted_device} // 0; 431 | push @rows, ["2FA Logins", $two_fa_count, "Total 2FA authentications"]; 432 | push @rows, ["Trusted Device Logins", $trusted_count, "Total trusted device authentications"]; 433 | 434 | print format_table(['Type', 'Value/Time', 'Notes'], \@rows) if @rows; 435 | print "\n"; 436 | } 437 | 438 | # Show IP history 439 | if (exists $users{$target_user}{ips}) { 440 | print colored(['bold'], "IP Address History:\n"); 441 | my @rows; 442 | foreach my $ip (sort keys %{$users{$target_user}{ips}}) { 443 | push @rows, [ 444 | $ip, 445 | $users{$target_user}{ips}{$ip}{last_seen} // 'N/A', 446 | $users{$target_user}{ips}{$ip}{count} // 0 447 | ]; 448 | } 449 | print format_table(['IP Address', 'Last Seen', 'Access Count'], \@rows); 450 | print "\n"; 451 | } 452 | 453 | # Show authentication methods 454 | if (exists $auth_methods{$target_user}) { 455 | print colored(['bold'], "Authentication Summary:\n"); 456 | my @rows; 457 | push @rows, ["Two-Factor Auth", $auth_methods{$target_user}{two_factor_auth} // 0]; 458 | push @rows, ["Trusted Device", $auth_methods{$target_user}{trusted_device} // 0]; 459 | push @rows, ["App-Specific Password", $auth_methods{$target_user}{app_specific} // 0]; 460 | push @rows, ["POP3 Access", $auth_methods{$target_user}{pop3} // 0]; 461 | push @rows, ["IMAP Access", $auth_methods{$target_user}{imap} // 0]; 462 | print format_table(['Method', 'Count'], \@rows); 463 | print "\n"; 464 | } 465 | } 466 | 467 | # Table formatting function 468 | sub format_all_table { 469 | my ($headers, $rows) = @_; 470 | my @col_widths; 471 | 472 | # Calculate column widths 473 | for my $col (0..$#$headers) { 474 | 475 | # Special handling for IP address column 476 | if ($col == 3) { # IP Addresses column 477 | $col_widths[$col] = (15 * 4) + (2 * 3) + 2; # 4 IPs * 15 chars + 3 separators * 2 chars + padding 478 | } else { 479 | # Get substring up to the first newline, if present 480 | my $header_value = $headers->[$col] =~ /\n/ ? (split(/\n/, $headers->[$col]))[0] : $headers->[$col]; 481 | my $max_width = length($header_value); 482 | for my $row (@$rows) { 483 | my $len_value = $row->[$col] =~ /\n/ ? (split(/\n/, $row->[$col]))[0] : $row->[$col]; 484 | my $len = length($len_value); 485 | $max_width = $len if $len > $max_width; 486 | } 487 | $col_widths[$col] = $max_width + 2; # Add padding 488 | } 489 | } 490 | 491 | # Calculate IP column start position (sum of previous column widths plus separators) 492 | my $ip_column_start = 1; # Start after first | 493 | for my $i (0..2) { # Add widths of first three columns 494 | $ip_column_start += $col_widths[$i] + 1; # +1 for each separator 495 | } 496 | 497 | # Format header 498 | my $separator = '+' . join('+', map {'-' x $_} @col_widths) . '+'; 499 | my $output = $separator . "\n"; 500 | $output .= '|' . join('|', map {sprintf("%-*s", $col_widths[$_], " $headers->[$_]")} 0..$#$headers) . "|\n"; 501 | $output .= $separator . "\n"; 502 | 503 | # Format rows with proper multi-line handling 504 | for my $row (@$rows) { 505 | my @formatted_columns = (); 506 | 507 | # Handle first three columns normally 508 | for my $col (0..2) { 509 | my $cell_value = $row->[$col] // ''; 510 | push @formatted_columns, sprintf("%-*s", $col_widths[$col], " $cell_value"); 511 | } 512 | 513 | # Special handling for IP address column 514 | my $ip_value = $row->[3] // ''; 515 | my @ip_lines = split(/\n/, $ip_value); 516 | 517 | # Format first line of IPs 518 | push @formatted_columns, sprintf("%-*s", $col_widths[3], " $ip_lines[0]"); 519 | 520 | # Output the first line 521 | $output .= '|' . join('|', @formatted_columns) . "|\n"; 522 | 523 | # Output continuation lines for IPs if they exist 524 | for my $i (1..$#ip_lines) { 525 | $output .= '|' . (' ' x ($ip_column_start - 1)) . sprintf("%-*s", $col_widths[3], " $ip_lines[$i]") . "|\n"; 526 | } 527 | 528 | } 529 | 530 | $output .= $separator . "\n"; 531 | return $output; 532 | } 533 | 534 | # Table formatting function 535 | # Table formatting function 536 | sub format_table { 537 | my ($headers, $rows) = @_; 538 | my @col_widths; 539 | 540 | # Calculate column widths 541 | for my $col (0..$#$headers) { 542 | my $max_width = length($headers->[$col]); 543 | for my $row (@$rows) { 544 | my $len = length($row->[$col] // ''); 545 | $max_width = $len if $len > $max_width; 546 | } 547 | $col_widths[$col] = $max_width + 2; # Add padding 548 | } 549 | 550 | # Format header 551 | my $separator = '+' . join('+', map {'-' x $_} @col_widths) . '+'; 552 | my $output = $separator . "\n"; 553 | $output .= '|' . join('|', map {sprintf("%-*s", $col_widths[$_], " $headers->[$_]")} 0..$#$headers) . "|\n"; 554 | $output .= $separator . "\n"; 555 | 556 | # Format rows 557 | for my $row (@$rows) { 558 | $output .= '|' . join('|', map {sprintf("%-*s", $col_widths[$_], " " . ($row->[$_] // ''))} 0..$#$headers) . "|\n"; 559 | } 560 | $output .= $separator . "\n"; 561 | 562 | return $output; 563 | } 564 | 565 | -------------------------------------------------------------------------------- /src/zmAveSizeMsg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Author: Jim Dunphy (2/28/2025) 5 | # 6 | # Refernce: https://forums.zimbra.org/viewtopic.php?t=73274 7 | # 8 | # Note: If you have 100 users, it will query 100 tables. 9 | # If you require a faster version then use which is very fast. 10 | # 11 | # zmAveSizeMsgStoredProcedure.sh 12 | # 13 | # usage: zmAveSizeMsg.sh 14 | # 15 | # Determine the average size message this zimbra server handles. 16 | # 17 | # Processing database: mboxgroup1 (this may take a moment...) 18 | # Processing database: mboxgroup10 (this may take a moment...) 19 | # Processing database: mboxgroup11 (this may take a moment...) 20 | # ... 21 | # Processing database: mboxgroupN (this may take a moment...) 22 | # Overall average email size for the entire system: 110.41 KB 23 | # 24 | # Caveat: Must be run as the zimbra user as we need the environmental variables for mysql 25 | 26 | # Check if the user is zimbra 27 | if [ "$(whoami)" != "zimbra" ]; then 28 | echo "Please run as zimbra" 29 | exit 1 30 | fi 31 | 32 | # Zimbra MySQL credentials 33 | ZMYSQL_USER="zimbra" 34 | ZMYSQL_PASSWORD="your_mysql_password" # Replace with your Zimbra MySQL password 35 | ZMYSQL_HOST="localhost" 36 | 37 | # Function to calculate average using awk 38 | calculate_average() { 39 | local total_size=$1 40 | local total_emails=$2 41 | echo "$total_size $total_emails" | awk '{printf "%.2f", ($1 / $2) / 1024}' 42 | } 43 | 44 | # Get the list of all mboxgroup databases 45 | #MBOXGROUPS=$(mysql -h $ZMYSQL_HOST -u $ZMYSQL_USER -p$ZMYSQL_PASSWORD -e "SHOW DATABASES LIKE 'mboxgroup%';" -s -N) 46 | MBOXGROUPS=$(mysql -u $ZMYSQL_USER -e "SHOW DATABASES LIKE 'mboxgroup%';" -s -N) 47 | 48 | # Check if mboxgroup databases exist 49 | if [ -z "$MBOXGROUPS" ]; then 50 | echo "No mboxgroup databases found!" 51 | exit 1 52 | fi 53 | 54 | # Initialize variables for total size and total email count 55 | TOTAL_SIZE=0 56 | TOTAL_EMAILS=0 57 | 58 | # Loop through each mboxgroup database 59 | for DB in $MBOXGROUPS; do 60 | echo "Processing database: $DB (this may take a moment...)" 61 | 62 | # Query the total size and email count for the current mboxgroup 63 | #RESULTS=$(mysql -h $ZMYSQL_HOST -u $ZMYSQL_USER -p$ZMYSQL_PASSWORD -D $DB -e "SELECT SUM(size), COUNT(*) FROM mail_item WHERE type = 5;" -s -N) 64 | RESULTS=$(mysql -u $ZMYSQL_USER -D $DB -e "SELECT SUM(size), COUNT(*) FROM mail_item WHERE type = 5;" -s -N) 65 | 66 | # Extract the total size and email count from the results 67 | SIZE=$(echo "$RESULTS" | awk '{print $1}') 68 | COUNT=$(echo "$RESULTS" | awk '{print $2}') 69 | 70 | # Accumulate total size and email count 71 | TOTAL_SIZE=$((TOTAL_SIZE + SIZE)) 72 | TOTAL_EMAILS=$((TOTAL_EMAILS + COUNT)) 73 | done 74 | 75 | # Calculate the overall average email size in KB 76 | if [ $TOTAL_EMAILS -gt 0 ]; then 77 | OVERALL_AVG=$(calculate_average $TOTAL_SIZE $TOTAL_EMAILS) 78 | echo "Overall average email size for the entire system: $OVERALL_AVG KB" 79 | else 80 | echo "No emails found in any mboxgroup database!" 81 | fi 82 | -------------------------------------------------------------------------------- /src/zmAveSizeMsgStoredProcedure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Author: Jim Dunphy (2/28/2025) 5 | # 6 | # Refernce: https://forums.zimbra.org/viewtopic.php?t=73274 7 | # 8 | # 9 | # usage: zmAveSizeMsgStoredProcedure.sh 10 | # Calculating average email size... 11 | # +-----------------------+ 12 | # | average_email_size_kb | 13 | # +-----------------------+ 14 | # | 111.93 | 15 | # +-----------------------+ 16 | # 17 | # Caveat: must be run as the zimbra user 18 | # 19 | 20 | # Check if the user is zimbra 21 | if [ "$(whoami)" != "zimbra" ]; then 22 | echo "Please run as zimbra" 23 | exit 1 24 | fi 25 | 26 | # Zimbra MySQL credentials 27 | ZMYSQL_USER="zimbra" 28 | ZMYSQL_PASSWORD="your_mysql_password" # Replace with your Zimbra MySQL password 29 | ZMYSQL_HOST="localhost" 30 | 31 | # Function to install the stored procedure 32 | install_stored_procedure() { 33 | #mysql -h $ZMYSQL_HOST -u $ZMYSQL_USER -p$ZMYSQL_PASSWORD mysql < 0 THEN 82 | SET avg_size_kb = (total_size / total_emails) / 1024; 83 | SELECT avg_size_kb AS average_email_size_kb; 84 | ELSE 85 | SELECT 'No emails found in any mboxgroup database!' AS message; 86 | END IF; 87 | END // 88 | 89 | DELIMITER ; 90 | EOF 91 | } 92 | 93 | # Function to remove the stored procedure 94 | remove_stored_procedure() { 95 | #mysql -h $ZMYSQL_HOST -u $ZMYSQL_USER -p$ZMYSQL_PASSWORD mysql -e "DROP PROCEDURE IF EXISTS CalculateAverageEmailSize;" 96 | mysql -u $ZMYSQL_USER mysql -e "DROP PROCEDURE IF EXISTS CalculateAverageEmailSize;" 97 | } 98 | 99 | # Install the stored procedure 100 | install_stored_procedure 101 | 102 | # Call the stored procedure and display the results 103 | echo "Calculating average email size..." 104 | #mysql -h $ZMYSQL_HOST -u $ZMYSQL_USER -p$ZMYSQL_PASSWORD mysql -e "CALL CalculateAverageEmailSize();" 105 | mysql -u $ZMYSQL_USER mysql -e "CALL CalculateAverageEmailSize();" 106 | 107 | # Remove the stored procedure (optional) 108 | remove_stored_procedure 109 | -------------------------------------------------------------------------------- /src/zmShowUserCounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Author: Jim Dunphy 5 | # License (ISC): It's yours. Enjoy 6 | # 4/16/2023 7 | # 8 | # 9 | # Caveat: Needs to be run as root 10 | # installs a stored procedure call that remains until you drop it from the database. 11 | # 12 | # Note: 13 | # speeds it up substantically from 46 seconds to 2.3 seconds on tests here 14 | # 15 | # 16 | # Sample output: 17 | # 18 | #User Total Messages Total Folders Total Contacts 19 | #-------------------------------------------------------------------------------------------------------------- 20 | #user1@xxxxxxxxxxxxxx.com 0 17 0 21 | #user2@xxxxxxxxxxxxxx.com 0 16 1 22 | #user3@xxxxxxxxxxxxxx.com 19014 115 2314 23 | #... 24 | # 25 | # 26 | # Folder (type = 1): Represents a folder in the mailbox. 27 | # Tag (type = 2): Represents a tag that can be associated with items in the mailbox. 28 | # Conversation (type = 3): Represents a conversation, which is a group of related email messages. 29 | # Message (type = 5): Represents an individual email message. 30 | # Contact (type = 6): Represents a contact in the user's address book. 31 | # Document (type = 7): Represents a document saved in the user's mailbox. 32 | # Appointment (type = 11): Represents a calendar appointment. 33 | # Task (type = 13): Represents a task in the user's task list. 34 | # Wiki (type = 14): Represents a wiki page in the user's mailbox. 35 | # Chat (type = 15): Represents a chat message in the user's mailbox. 36 | # 37 | # These are the most common types you may encounter when working with Zimbra's MySQL database. The 38 | # type values are used to identify the specific kind of item in the mail_item table. 39 | # 40 | 41 | dropStoredProcedure() { 42 | su - zimbra -c "mysql <<'EOF' 43 | USE zimbra; 44 | DROP PROCEDURE getUserStats; 45 | EOF" 46 | } 47 | 48 | addStoredProcedure() { 49 | su - zimbra -c "mysql <<'EOF' 50 | 51 | USE zimbra; 52 | 53 | DELIMITER // 54 | CREATE PROCEDURE getUserStats() 55 | BEGIN 56 | DECLARE done INT DEFAULT FALSE; 57 | DECLARE current_group_id INT; 58 | DECLARE cur CURSOR FOR SELECT DISTINCT group_id FROM mailbox; 59 | DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; 60 | 61 | DROP TEMPORARY TABLE IF EXISTS temp_user_stats; 62 | CREATE TEMPORARY TABLE temp_user_stats ( 63 | User VARCHAR(255), 64 | TotalMessages INT, 65 | TotalFolders INT, 66 | TotalContacts INT 67 | ); 68 | 69 | OPEN cur; 70 | 71 | read_loop: LOOP 72 | FETCH cur INTO current_group_id; 73 | IF done THEN 74 | LEAVE read_loop; 75 | END IF; 76 | 77 | SET @query = CONCAT(' 78 | INSERT INTO temp_user_stats 79 | SELECT mbox.comment AS User, 80 | SUM(IF(mi.type = 5, 1, 0)) AS TotalMessages, 81 | SUM(IF(mi.type = 1, 1, 0)) AS TotalFolders, 82 | SUM(IF(mi.type = 6, 1, 0)) AS TotalContacts 83 | FROM mailbox AS mbox 84 | JOIN mboxgroup', current_group_id, '.mail_item AS mi 85 | ON mbox.id = mi.mailbox_id 86 | GROUP BY mbox.id, mbox.comment; 87 | '); 88 | 89 | PREPARE stmt FROM @query; 90 | EXECUTE stmt; 91 | DEALLOCATE PREPARE stmt; 92 | END LOOP; 93 | 94 | CLOSE cur; 95 | 96 | SELECT * FROM temp_user_stats; 97 | END// 98 | DELIMITER ; 99 | EOF" 100 | 101 | } 102 | 103 | usage() { 104 | echo " 105 | 106 | Note: Stored procedure call must be added before results can be generated 107 | where it will remain with the database until manually dropped. 108 | 109 | % su - 110 | # zmShowUserCounts.sh --add # one time only 111 | # zmShowUserCounts.sh # generate results 112 | # zmShowUserCounts.sh --help 113 | # zmShowUserCounts.sh --version 114 | # zmShowUserCounts.sh --drop 115 | 116 | 117 | If you want to remove this stored procedure, use the --drop option 118 | 119 | # zmShowUserCounts.sh --drop 120 | 121 | " 122 | } 123 | 124 | # need to run as root to make su to zimbra transparent 125 | ID=`id -u` 126 | if [ "x$ID" != "x0" ]; then 127 | echo "Run as root!" 128 | exit 1 129 | fi 130 | 131 | 132 | args=$(getopt -l "add,drop,help,version" -o "adhv" -- "$@") 133 | 134 | eval set -- "$args" 135 | 136 | while [ $# -ge 1 ]; do 137 | case "$1" in 138 | --) 139 | # No more options left. 140 | shift 141 | break 142 | ;; 143 | -a | --add) 144 | addStoredProcedure 145 | exit 146 | ;; 147 | -d | --drop) 148 | dropStoredProcedure 149 | exit 150 | ;; 151 | -v | --version) 152 | echo "Version 0.2" 153 | exit 154 | ;; 155 | -h|--help) 156 | usage 157 | exit 0 158 | ;; 159 | esac 160 | 161 | shift 162 | done 163 | 164 | 165 | # Define a command string to be executed as the zimbra user 166 | commands=$(cat <<'EOF' 167 | zimbra_mysql_password=$(zmlocalconfig -s zimbra_mysql_password | awk '{print $3}') 168 | 169 | printf "%-60s %-16s %-16s %-16s\n" "User" "Total Messages" "Total Folders" "Total Contacts" 170 | echo "-------------------------------------------------------------------------------------------------------------" 171 | 172 | IFS=$'\n' 173 | rows=$(mysql --user=zimbra --password=$zimbra_mysql_password -N -e "use zimbra;CALL getUserStats()") 174 | for row in $rows; do 175 | username=$(echo "$row" | awk '{print $1}') 176 | total_messages=$(echo "$row" | awk '{print $2}') 177 | total_folders=$(echo "$row" | awk '{print $3}') 178 | total_contacts=$(echo "$row" | awk '{print $4}') 179 | printf "%-60s %-16s %-16s %-16s\n" "$username" "$total_messages" "$total_folders" "$total_contacts" 180 | done 181 | EOF 182 | ) 183 | 184 | # Execute the commands as the zimbra user 185 | su - zimbra -c "$commands" 186 | -------------------------------------------------------------------------------- /src/zmbounceMsg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Jim Dunphy 4 | # License (ISC): It's yours. Enjoy 5 | # Date: 2/9/2019 6 | # 7 | # usage: zmbounceMsg Message-ID 8 | # 9 | # Find quarantined message and release back to user based on Message-ID. That ID was sent to the user 10 | # via email. It will handle the multiple user case for the same Message-ID that exists in different files. 11 | # 12 | # Caveat: It doesn't execute the command but explains the commands you would run. 13 | # Note: Administrator needs to verify the file before releasing the file 14 | # 15 | 16 | 17 | PATH=$PATH:/usr/bin:/sbin:/usr/sbin:/bin export PATH 18 | 19 | debug=0 20 | justOnce=1 21 | 22 | _d () { 23 | [ $debug ] && echo $1 24 | } 25 | 26 | # Need Message-ID from the email sent to the user about quanrantine 27 | if [ $# -ne 1 ]; then 28 | echo $0: usage: zmbounceMsg Message-ID 29 | exit 1 30 | else 31 | MessageId="$1" 32 | fi 33 | 34 | # Only zimbra user 35 | if [ x`whoami` != xzimbra ]; then 36 | echo Error: must be run as zimbra user 37 | exit 1 38 | fi 39 | 40 | #Get quarantine account 41 | read virusAcct <<< $(zmprov gcf zimbraAmavisQuarantineAccount | awk -F: '{print $2}') 42 | _d "virus account: $virusAcct" 43 | 44 | #Locate mailbox id for quarantine account 45 | read mailboxId <<< $(zmprov gmi "$virusAcct" | grep mailboxId | awk '{print $2}') 46 | _d "mailbox id: $mailboxId" 47 | 48 | _d "location: /opt/zimbra/store/0/$mailboxId/msg/0" 49 | #Locate message to bounce to user 50 | if [ -d /opt/zimbra/store/0/$mailboxId/msg/0 ]; then 51 | cd /opt/zimbra/store/0/$mailboxId/msg/0 52 | for filename in * ; do 53 | if head -100 $filename | grep -i Message-ID | grep -qi $MessageId; then 54 | if [ $justOnce -eq 1 ];then justOnce=0;echo "Run these commands";echo "cd /opt/zimbra/store/0/$mailboxId/msg/0";fi 55 | #_d "$filename selected" 56 | to=$(head -50 $filename | grep "^X-Envelope-To-Blocked:" | awk '{print $2}' | sed 's/["\n\r<>]//g' | head -1) 57 | from=$(head -50 $filename | grep "^X-Envelope-From:" | awk '{print $2}' | sed 's/["\n\r<>]//g' | head -1) 58 | if [ ! "x$to" = x ];then 59 | echo "zmlmtpinject -r $to -s $from $filename" 60 | fi 61 | fi 62 | done 63 | fi 64 | 65 | exit 0 66 | -------------------------------------------------------------------------------- /src/zmcertNotice.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Notify us X days in advance of pending renewal for acme.sh letsencrypt renewal. 4 | # 5 | # Verify you have this script in your zimbra crontab entry. Should look something like this. 6 | # 7 | # # ZIMBRAEND -- DO NOT EDIT ANYTHING BETWEEN THIS LINE AND ZIMBRASTART 8 | # 9 | # 18 0 * * * "/opt/zimbra/.acme.sh"/acme.sh --cron --home "/opt/zimbra/.acme.sh" > /dev/null 10 | # 17 0 * * * /usr/local/bin/zmcertNotice.sh > /dev/null 2>&1 11 | # 12 | # Bug fix: acme.sh version 3.05 changed the date format for ./acme.sh --list (workaround: 10/31/2022) 13 | # 14 | # You need to supply your email at STEP 1. There is only 1 step :-) 15 | # 16 | 17 | export PATH=~/.acme.sh:/bin:/usr/bin:/usr/sbin:/usr/local:$PATH 18 | 19 | # 20 | # Quick/Dirty script to notify us via email the day 21 | # prior to a letsencrypt renew cycle by acme.sh 22 | # 23 | # 24 | email="XXX@XXXX.com" # %%% STEP 1: CHANGE THIS 25 | sendmail="/opt/zimbra/common/sbin/sendmail" 26 | 27 | domainCert=$(acme.sh --list | sed 1d | head -1 | awk '{printf "%s",$3}') 28 | subject="$domainCert Certificate renewal in 1 day(s)" 29 | message="Hello, 30 | this is a reminder that $domainCert letsencrypt certificate 31 | will try to obtain and install new zimbra certificate in 1 day(s)." 32 | 33 | #------------------------------- 34 | # What acme.sh thinks when it could renew 35 | 36 | renewalDate=$(acme.sh --list | sed 1d | head -1 | awk '{printf "%s %s %s %s %s %s",$11,$12,$13,$14,$15,$16}') 37 | if [ "$renewalDate" = " " ]; then 38 | #echo "new format" 39 | # 2022-10-30T17:14:29Z --- need to convert to 2022-10-30 17:14:29 40 | renewalDate=$(acme.sh --list | sed 1,"$domain"d | head -1 | awk '{printf "%s",$6}' | sed 's/T/ /' | sed 's/Z//') 41 | fi 42 | 43 | # first renewal date 44 | cmd="date +%s -d \"$renewalDate\"" 45 | r_time=$(eval $cmd) 46 | 47 | #------------------------------- 48 | # we want to know 2 days in advance 49 | currentDate=$(date -u) # now 50 | 51 | cmd="date +%s -d \"$currentDate\"" 52 | currentDateInSecs=$(eval $cmd) # now in seconds 53 | cmd="date +%s -d \"$currentDate+2 days\"" 54 | r_time_in_future=$(eval $cmd) # now + 2 days in seconds 55 | 56 | #------------------------------- 57 | 58 | if ((( $currentDateInSecs > $r_time )) || (($r_time_in_future > $r_time))) ; then 59 | echo "will renew in 1 day" 60 | 61 | echo "Subject: $subject 62 | 63 | $message" | "$sendmail" "$email" 64 | fi 65 | -------------------------------------------------------------------------------- /src/zmcertNotice2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ============================================================================ 3 | # zmcertNotice.sh - Certificate renewal notification script 4 | # 5 | # Sends consolidated email notification when Let's Encrypt certificates are 6 | # approaching renewal time or have expired 7 | 8 | # 9 | # Notify us X days in advance of pending renewal for acme.sh letsencrypt renewal. 10 | # 11 | # Verify you have this script in your zimbra crontab entry. Should look something like this. 12 | # 13 | # # ZIMBRAEND -- DO NOT EDIT ANYTHING BETWEEN THIS LINE AND ZIMBRASTART 14 | # 15 | # 18 0 * * * "/opt/zimbra/.acme.sh"/acme.sh --cron --home "/opt/zimbra/.acme.sh" > /dev/null 16 | # 17 0 * * * /usr/local/bin/zmcertNotice.sh > /dev/null 2>&1 17 | # 18 | # Version 3.05 changed the date format for ./acme.sh --list 19 | # 20 | # You need to supply your email at STEP 1. There is only 1 step :-) 21 | # 22 | # 23 | 24 | # ============================================================================ 25 | 26 | # Set path to ensure acme.sh is available 27 | export PATH=~/.acme.sh:/bin:/usr/bin:/usr/sbin:/usr/local:$PATH 28 | 29 | # Configuration 30 | LOCAL_HOST=$(hostname) 31 | EMAIL="XXX@XXXX.com" # %%% STEP 1: CHANGE THIS 32 | SENDMAIL="/opt/zimbra/common/sbin/sendmail" 33 | NOTIFICATION_DAYS=1 # Days before renewal to send notification 34 | 35 | # Function to convert date to seconds since epoch 36 | date_to_seconds() { 37 | date +%s -d "$1" 38 | } 39 | 40 | # Create a temporary file to store certificates needing attention 41 | TEMP_FILE=$(mktemp) 42 | trap 'rm -f $TEMP_FILE' EXIT 43 | 44 | # Get current date in seconds 45 | current_date=$(date -u) 46 | current_seconds=$(date_to_seconds "$current_date") 47 | future_seconds=$(date_to_seconds "$current_date+$NOTIFICATION_DAYS days") 48 | 49 | # Get the list of domains and process each one 50 | acme.sh --list | tail -n +2 | while read -r line; do 51 | # Skip empty lines 52 | [ -z "$line" ] && continue 53 | 54 | # Extract Main Domain from first column 55 | domain=$(echo "$line" | awk '{print $1}') 56 | 57 | # Extract renewal date (last column) 58 | renewal_date=$(echo "$line" | awk '{print $NF}') 59 | 60 | # Convert T and Z to spaces for date command compatibility 61 | formatted_renewal_date=$(echo "$renewal_date" | sed 's/T/ /' | sed 's/Z//') 62 | 63 | # Calculate seconds for renewal time 64 | renewal_seconds=$(date_to_seconds "$formatted_renewal_date") 65 | 66 | # Determine if certificate needs attention 67 | if (( current_seconds > renewal_seconds )); then 68 | # Certificate is expired 69 | days_expired=$(( (current_seconds - renewal_seconds) / 86400 )) 70 | echo "$domain – EXPIRED $days_expired day(s) ago (renew was $(echo $renewal_date | sed 's/T.*Z//'))" >> "$TEMP_FILE" 71 | elif (( future_seconds > renewal_seconds )); then 72 | # Certificate is approaching renewal 73 | days_until_renewal=$(( (renewal_seconds - current_seconds) / 86400 )) 74 | echo "$domain – Renewal in $days_until_renewal day(s) (renew date: $(echo $renewal_date | sed 's/T.*Z//'))" >> "$TEMP_FILE" 75 | fi 76 | done 77 | 78 | # Check if we have any certificates needing attention 79 | if [ -s "$TEMP_FILE" ]; then 80 | # Build the email message 81 | subject="Certificate renewal alert for $LOCAL_HOST" 82 | 83 | message="Hello, 84 | 85 | The following Let's Encrypt certificates on $LOCAL_HOST need attention: 86 | 87 | " 88 | 89 | # Add each certificate as a bullet point 90 | while read -r cert; do 91 | message+=" • $cert 92 | " 93 | done < "$TEMP_FILE" 94 | 95 | message+=" 96 | This notice will repeat nightly until each certificate is renewed." 97 | 98 | # Send the email 99 | echo "Subject: $subject 100 | 101 | $message" | "$SENDMAIL" "$EMAIL" 102 | 103 | echo "Notification sent for $(wc -l < "$TEMP_FILE") certificates" 104 | fi 105 | 106 | exit 0 107 | -------------------------------------------------------------------------------- /src/zmcopyTrain: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Jim Dunphy 4 | # License (ISC): It's yours. Enjoy 5 | # Date: 12/7/2018 6 | # 7 | # usage: zmcopySpam 8 | # 9 | # Will create a directory under /tmp owned by zimbra containing all the ham and spam directly 10 | # trained by the users. Purpose is to allows admin to fine tune spam rules. 11 | # 12 | # Note: It isn't destructive so all normal processing by 13 | # zmtrainsa continues to work. This grabs a copy only. 14 | # 15 | # Background Information: 16 | # If a user hits the junk button, that email is sent to a special spam user. 17 | # If you drag a message to the junk folder, it is sent to a special spam user. 18 | # If you move a message into a junk folder via a filter and action, it is sent to a special spam user. 19 | # Same is true for the ham case. 20 | # This script pulls those emails from both the ham and spam account so the admin can look at why the 21 | # email wasn't properly classified. 22 | # 23 | # Forum Reference: https://forums.zimbra.org/viewtopic.php?f=15&t=65303 24 | # 25 | # ===================================================== 26 | # 27 | # 28 | 29 | DumpTrainSystem() { 30 | 31 | spamdir=`mktmpdir spam` 32 | hamdir=`mktmpdir ham` 33 | echo "Pulling spamassassin data." 34 | /opt/zimbra/libexec/zmspamextract -s -o ${spamdir} 35 | /opt/zimbra/libexec/zmspamextract -n -o ${hamdir} 36 | 37 | # only show if we have data (ham) 38 | if [ "$(ls -A ${hamdir})" ]; then 39 | echo "See ${hamdir} for ham" 40 | chmod -R 777 ${hamdir} 41 | else 42 | rmdir ${hamdir} 43 | fi 44 | 45 | # only show if we have data (spam) 46 | if [ "$(ls -A ${spamdir})" ]; then 47 | echo "See ${spamdir} for spam" 48 | chmod -R 777 ${spamdir} 49 | else 50 | rmdir ${spamdir} 51 | fi 52 | } 53 | 54 | mktmpdir() { 55 | mktemp -d "/tmp/zmtrainsa.$$.$1.XXXXXX" || exit 1 56 | } 57 | 58 | if [ x`whoami` != xzimbra ]; then 59 | echo Error: must be run as zimbra user 60 | exit 1 61 | fi 62 | 63 | cd /opt/zimbra 64 | source /opt/zimbra/bin/zmshutil || exit 1 65 | zmsetvars 66 | 67 | DumpTrainSystem 68 | 69 | exit 0 70 | -------------------------------------------------------------------------------- /src/zmdu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Printout email disk usage for all accounts in human readable form 4 | # 5 | # usage: zmdu.sh 6 | # 7 | # Required: NEEDS to RUN as THE ZIMBRA USER 8 | # 9 | # Author: Jim Dunphy, assist: Klug from Zimbra forums. 10 | # 11 | # Sorted by highest users 12 | # 13 | # su - zimbra 14 | # % zmdu.sh 15 | # 3G ... jim@example.com 16 | # 3G ... betsy@example.com 17 | # 696M ... anna@example.com 18 | # 72M ... sam@example.com 19 | # 67M ... helen@example.com 20 | # 12M ... noc@example.com 21 | # 460K ... spam.trivkbr1q@example.com 22 | # 27K ... wiki@mail.example.com 23 | # 17K ... ham.lub2tukfun@example.com 24 | # 15K ... wiki@example.com 25 | # 54 ... virus-quarantine.t98l1ltk1a@example.com 26 | # 0 ... admin@example.com 27 | # 28 | 29 | zmprov gqu $(zmhostname) | awk '{ $2="";suffix=" KMGT"; for(i=1; $3>1024 && i < length(suffix); i++) $3/=1024; printf "%6s%c ... %s\n", int($3),substr(suffix, i, 1), $1; }' | sort -rh 30 | 31 | exit 0 32 | --------------------------------------------------------------------------------