├── LICENSE ├── README.md ├── config.py ├── fix.py ├── media ├── dashboard.png ├── hosts.gif └── packages.gif ├── os-report ├── report.py └── scan_modules │ ├── __init__.py │ ├── centos_detect.py │ ├── debian_detect.py │ ├── linux_detect.py │ ├── nix_detect.py │ └── os_detect.py ├── prepare.py ├── requirements.txt ├── scan.py └── ztc.conf /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### :bangbang: Updated Zabbix Threat Control to version 2.0 :bangbang: 2 | 💥 Update breaks the plugin's normal operation!
3 | To make it work, please read the [Update instructions](https://github.com/vulnersCom/zabbix-threat-control/issues/16).
4 | And there's live-chat in Telegram, for technical support use our Telegram live-chat: [@ztcsupport](https://t.me/ztcsupport) 5 | 6 | ---- 7 | 8 | # Zabbix Threat Control 9 | 10 | Оur plugin transforms your Zabbix monitoring system into vulnerability, risk and security managment system for your infrastructure. 11 | 12 | * [What the plugin does](#what-the-plugin-does) 13 | * [How the plugin works](#how-the-plugin-works) 14 | * [Requirements](#requirements) 15 | * [Installation](#installation) 16 | * [Сonfiguration](#configuration) 17 | * [Execution](#execution) 18 | * [Usage](#usage) 19 | 20 | ## What the plugin does 21 | 22 | It provides Zabbix with information about vulnerabilities existing in your entire infrastructure and suggests easily applicable remediation plans. 23 | 24 | ![](https://github.com/vulnersCom/zabbix-threat-control/blob/master/media/dashboard.png) 25 | 26 | Information is displayed in Zabbix in the following format: 27 | 28 | - Maximum CVSS score for each server. 29 | - Command for fixing all detected vulnerabilities for each server. 30 | - List of security bulletins with descriptions for vulnerable packages valid for your infrastructure. 31 | - List of all vulnerable packages in your infrastructure. 32 | 33 | 34 | ![](https://github.com/vulnersCom/zabbix-threat-control/blob/master/media/hosts.gif) 35 | 36 | 37 | Security bulletins and packages information includes: 38 | 39 | - Impact index for the infrastructure. 40 | - CVSS score of a package or a bulletin. 41 | - Number of affected servers. 42 | - A detailed list of affected hosts. 43 | - Hyperlink to the description of a bulletin. 44 | 45 | ![](https://github.com/vulnersCom/zabbix-threat-control/blob/master/media/packages.gif) 46 | 47 | Sometimes it is impossible to update all packages on all servers to a version that fixes existing vulnerabilities. The proposed representation permits you to selectively update servers or packages. 48 | 49 | This approach allows one to fix vulnerabilities using different strategies: 50 | 51 | - all vulnerabilities on a specific server; 52 | - a single vulnerability in the entire infrastructure. 53 | 54 | This can be done directly from Zabbix (using its standard functionality) either on the administrator command or automatically. 55 | 56 | ## How the plugin works 57 | 58 | - Using Zabbix API, the plugin receives lists of installed packages, names and versions of the OS from all the servers in the infrastructure (if the "Vulners OS-Report" template is linked with them). 59 | - Transmits the data to Vulners 60 | - Receives information on the vulnerabilities for each server. 61 | - Processes the received information, aggregates it and sends it back to Zabbix via zabbix-sender. 62 | - Finally the result is displayed in Zabbix. 63 | 64 | ## Requirements 65 | 66 | **On zabbix-server host:** 67 | 68 | - python 3 (only for ztc scripts) 69 | - python modules: pyzabbix, jpath, requests, vulners 70 | - zabbix version 3.4 is required to create a custom dashboard and a custom polling schedule. 71 | - zabbix-sender utility for sending data to zabbix-server. 72 | - zabbix-get utility for sending a command to fix vulnerabilities on the server. 73 | 74 | **On all the servers that require a vulnerability scan:** 75 | 76 | - zabbix-agent for collect data and run scripts. 77 | 78 | ## Installation 79 | 80 | ### RHEL, CentOS and other RPM-based 81 | 82 | rpm -Uhv https://repo.vulners.com/redhat/vulners-repo.rpm 83 | 84 | **On zabbix-server host:** 85 | 86 | yum install zabbix-threat-control-main zabbix-threat-control-host 87 | 88 | **On all the servers that require a vulnerability scan:** 89 | 90 | yum install zabbix-threat-control-host 91 | 92 | 93 | ### Debian and other debian-based 94 | 95 | wget https://repo.vulners.com/vulners-repo-py3.deb 96 | dpkg -i vulners-repo-py3.deb 97 | 98 | **On zabbix-server host:** 99 | 100 | apt-get update && apt-get install zabbix-threat-control-main zabbix-threat-control-host 101 | 102 | **On all the servers that require a vulnerability scan:** 103 | 104 | apt-get update && apt-get install zabbix-threat-control-host 105 | 106 | ### From source 107 | 108 | **On zabbix-server host:** 109 | 110 | git clone https://github.com/vulnersCom/zabbix-threat-control.git 111 | mkdir -p /opt/monitoring/zabbix-threat-control 112 | cp -R zabbix-threat-control/os-report /opt/monitoring/ 113 | cp zabbix-threat-control/*.py /opt/monitoring/zabbix-threat-control/ 114 | cp zabbix-threat-control/*.conf /opt/monitoring/zabbix-threat-control/ 115 | chown -R zabbix:zabbix /opt/monitoring/ 116 | chmod 640 /opt/monitoring/zabbix-threat-control/*.conf 117 | touch /var/log/zabbix-threat-control.log 118 | chown zabbix:zabbix /var/log/zabbix-threat-control.log 119 | chmod 664 /var/log/zabbix-threat-control.log 120 | 121 | **On all the servers that require a vulnerability scan:** 122 | 123 | git clone https://github.com/vulnersCom/zabbix-threat-control.git 124 | mkdir -p /opt/monitoring/ 125 | cp -R zabbix-threat-control/os-report /opt/monitoring/ 126 | chown -R zabbix:zabbix /opt/monitoring/os-report 127 | 128 | ## Configuration 129 | 130 | The configuration file is located here: `/opt/monitoring/zabbix-threat-control/ztc.conf` 131 | 132 | ### Vulners credentials 133 | 134 | To use Vulners API you need an api-key. To get it follow the steps bellow: 135 | - Log in to vulners.com. 136 | - Navigate to the userinfo space https://vulners.com/userinfo. 137 | - Choose the "API KEYS" section. 138 | - Select "scan" in the scope menu and click "Generate a new key". 139 | - You will get an api-key, which looks like this: 140 | **RGB9YPJG7CFAXP35PMDVYFFJPGZ9ZIRO1VGO9K9269B0K86K6XQQQR32O6007NUK** 141 | 142 | Now you need to add the Vulners api-key into your configuration file (parameter ```VulnersApiKey```). 143 | 144 | ``` 145 | VulnersApiKey = RGB9YPJG7CFAXP35PMDVYFFJPGZ9ZIRO1VGO9K9269B0K86K6XQQQR32O6007NUK 146 | ``` 147 | 148 | 149 | ### Zabbix credentials 150 | 151 | In order to connect to Zabbix you need to specify the following in the configuration file: 152 | - The URL, username and password. Note that the User should have rights to create groups, hosts and templates in Zabbix. 153 | - Domain name and port of the Zabbix-server for pushing data using the zabbix-sender. 154 | 155 | Here is an example of a valid config file: 156 | 157 | ``` 158 | ZabbixApiUser = yourlogin 159 | ZabbixApiPassword = yourpassword 160 | ZabbixFrontUrl = https://zabbixfront.yourdomain.com 161 | 162 | ZabbixServerFQDN = zabbixserver.yourdomain.com 163 | ZabbixServerPort = 10051 164 | ``` 165 | 166 | ### Zabbix entity 167 | 168 | 1. To create all the necessary objects in Zabbix, run the `prepare.py` script with parameters.
169 | `/opt/monitoring/zabbix-threat-control/prepare.py -uvtd`
It will verify that zabbix-agent and zabbix-get utilities are configured correctly and create the following objects using Zabbix API: 170 | * **A template** used to collect data from servers. 171 | * **Zabbix hosts** for obtaining data on vulnerabilities. 172 | * **An action** to run the command fixes the vulnerability. 173 | * **A dashboard** for displaying results. 174 | 2. While using the Zabbix web interface, it is necessary to link the "Vulners OS-Report" template with the hosts that you are doing a vulnerabilities scan on. 175 | 176 | ### Servers that require a vulnerability scan 177 | 178 | Zabbix-agent must be able to execute remote commands. For this, change the parameters in the zabbix-agent configuration file `/etc/zabbix/zabbix_agentd.conf`: 179 | 180 | ``` 181 | EnableRemoteCommands=1 182 | LogRemoteCommands=1 183 | ``` 184 | 185 | Zabbix-agent must be able to update packages as root. For this, add a line to the file `/etc/sudoers`: 186 | 187 | ``` 188 | zabbix ALL=(ALL) NOPASSWD: /usr/bin/yum -y update * 189 | zabbix ALL=(ALL) NOPASSWD: /usr/bin/apt-get --assume-yes install --only-upgrade * 190 | ``` 191 | 192 | ## Execution 193 | 194 | - `/opt/monitoring/os-report/report.py`
195 | Transfers the name, version and installed packages of the operating system to Zabbix.
196 | Runs with zabbix-agent on all hosts to which the template "Vulners OS-Report" is linked. 197 | 198 | - `/opt/monitoring/zabbix-threat-control/scan.py`
199 | Processes raw data from zabbix and vulners and push them to the monitoring system using zabbix-sender.
200 | Runs with zabbix-agent on the Zabbix server via the item "Service item" on the host "Vulners - Statistics". 201 | 202 | The above scripts are run once a day. The start-up time is selected randomly during the installation and does not change during operation. 203 | 204 | - `/opt/monitoring/zabbix-threat-control/fix.py`
205 | Runs commands to fix vulnerabilities on servers. It's executed as a remote command in the action "Vunlers" in Zabbix. 206 | 207 | 208 | ## Usage 209 | It will be ready soon... 210 | 211 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """Config reader for ZTC""" 2 | 3 | __version__ = "2.2" 4 | 5 | 6 | import configparser 7 | 8 | config = configparser.ConfigParser() 9 | config.read("/opt/monitoring/zabbix-threat-control/ztc.conf") 10 | 11 | vuln_api_key = config.get("MANDATORY", "VulnersApiKey", fallback=None) 12 | vuln_proxy_host = config.get("OPTIONAL", "VulnersProxyHost", fallback=None) 13 | 14 | zbx_url = config.get("OPTIONAL", "ZabbixFrontUrl", fallback="http://localhost") 15 | zbx_user = config.get("MANDATORY", "ZabbixApiUser", fallback=None) 16 | zbx_pwd = config.get("MANDATORY", "ZabbixApiPassword", fallback=None) 17 | zbx_verify_ssl = config["OPTIONAL"].getboolean("VerifySSL", True) 18 | 19 | zbx_server_fqdn = config.get("OPTIONAL", "ZabbixServerFQDN", fallback="localhost") 20 | zbx_server_port = config["OPTIONAL"].getint("ZabbixServerPort", 10051) 21 | 22 | use_zbx_agent_to_fix = config["OPTIONAL"].getboolean("UseZabbixAgentToFix", True) 23 | acknowledge_users = config.get( 24 | "OPTIONAL", "TrustedZabbixUsers", fallback="Admin" 25 | ).split(",") 26 | ssh_user = config.get("OPTIONAL", "SSHUser", fallback="root") 27 | 28 | log_file = config.get( 29 | "OPTIONAL", "logfile", fallback="/var/log/zabbix-threat-control.log" 30 | ) 31 | debug_level = config["OPTIONAL"].getint("DebugLevel", 1) 32 | work_dir = config.get( 33 | "OPTIONAL", "WorkDir", fallback="/opt/monitoring/zabbix-threat-control" 34 | ).rstrip("/") 35 | 36 | application_name = config.get( 37 | "OPTIONAL", "HostsApplicationName", fallback="Vulnerabilities" 38 | ) 39 | 40 | dash_name = config.get("OPTIONAL", "DashboardName", fallback="Vulners") 41 | action_name = config.get("OPTIONAL", "ActionName", fallback="Vulners") 42 | 43 | hosts_host = config.get("OPTIONAL", "HostsHost", fallback="vulners.hosts") 44 | hosts_name = config.get("OPTIONAL", "HostsVisibleName", fallback="Vulners - Hosts") 45 | 46 | bulletins_host = config.get("OPTIONAL", "BulletinsHost", fallback="vulners.bulletins") 47 | bulletins_name = config.get( 48 | "OPTIONAL", "BulletinsVisibleName", fallback="Vulners - Bulletins" 49 | ) 50 | 51 | packages_host = config.get("OPTIONAL", "PackagesHost", fallback="vulners.packages") 52 | packages_name = config.get( 53 | "OPTIONAL", "PackagesVisibleName", fallback="Vulners - Packages" 54 | ) 55 | 56 | statistics_host = config.get( 57 | "OPTIONAL", "StatisticsHost", fallback="vulners.statistics" 58 | ) 59 | statistics_name = config.get( 60 | "OPTIONAL", "StatisticsVisibleName", fallback="Vulners - Statistics" 61 | ) 62 | 63 | stats_macros_name = config.get( 64 | "OPTIONAL", "StatisticsMacrosName", fallback="{$WORK_SCRIPT_CMD}" 65 | ) 66 | stats_macros_value = config.get( 67 | "OPTIONAL", 68 | "StatisticsMacrosValue", 69 | fallback="/opt/monitoring/zabbix-threat-control/scan.py", 70 | ) 71 | 72 | template_host = config.get( 73 | "OPTIONAL", "TemplateHost", fallback="tmpl.vulners.os-report" 74 | ) 75 | template_name = config.get( 76 | "OPTIONAL", "TemplateVisibleName", fallback="Template Vulners OS-Report" 77 | ) 78 | 79 | template_macros_name = config.get( 80 | "OPTIONAL", "TemplateMacrosName", fallback="{$REPORT_SCRIPT_PATH}" 81 | ) 82 | template_macros_value = config.get( 83 | "OPTIONAL", "TemplateMacrosValue", fallback="/opt/monitoring/os-report/report.py" 84 | ) 85 | 86 | template_application_name = config.get( 87 | "OPTIONAL", "TemplateApplicationName", fallback="Vulners OS Report" 88 | ) 89 | 90 | group_name = config.get("OPTIONAL", "HostGroupName", fallback="Vulners") 91 | template_group_name = config.get("OPTIONAL", "TemplateGroupName", fallback="Templates") 92 | 93 | zabbix_sender_bin = config.get("OPTIONAL", "ZabbixSender", fallback="zabbix_sender") 94 | zabbix_get_bin = config.get("OPTIONAL", "ZabbixGet", fallback="zabbix_get") 95 | 96 | min_cvss = config["OPTIONAL"].getint("MinCVSS", 1) 97 | -------------------------------------------------------------------------------- /fix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | """ 5 | Zabbix vulnerability assessment plugin. 6 | 7 | Script will fix vulnerabilities. 8 | fix.py {HOST.HOST} {TRIGGER.ID} {EVENT.ID} 9 | """ 10 | 11 | __version__ = "2.2" 12 | 13 | 14 | import sys 15 | import logging 16 | import subprocess 17 | import config 18 | from pyzabbix import ZabbixAPI 19 | 20 | 21 | def shell(command): 22 | proc = subprocess.Popen( 23 | command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True 24 | ) 25 | out = proc.communicate()[0].decode("utf8") 26 | return out 27 | 28 | 29 | def do_fix(visual_name, fix_cmd): 30 | hosts = zapi.host.get(filter={"name": visual_name}, output=["hostid"]) 31 | if len(hosts) == 0: 32 | logging.warning( 33 | "Can't find host {} in Zabbix. Skip fixing vulnerabilities on this host".format( 34 | visual_name 35 | ) 36 | ) 37 | exit(1) 38 | 39 | host_id = hosts[0]["hostid"] 40 | host_interface = zapi.hostinterface.get( 41 | hostids=host_id, 42 | filter={"main": "1", "type": "1"}, 43 | output=("dns", "ip", "useip", "port"), 44 | ) 45 | 46 | if not host_interface: 47 | logging.error("Host interface for hostid {} not found".format(host_id)) 48 | exit(1) 49 | 50 | cmd_params = { 51 | "zabbix_get_bin": config.zabbix_get_bin, 52 | "host_address": host_interface["ip" if int(host_interface["useip"]) else "dns"], 53 | "host_port": host_interface["port"], 54 | "fix_cmd": fix_cmd, 55 | } 56 | 57 | if config.use_zbx_agent_to_fix: 58 | cmd = '{zabbix_get_bin} -s {host_address} -p {host_port} -k "system.run[{fix_cmd},nowait]"'.format( 59 | **cmd_params 60 | ) 61 | else: 62 | cmd = 'ssh {} -l {} "{}"'.format( 63 | cmd_params["host_address"], config.ssh_user, fix_cmd 64 | ) 65 | 66 | logging.info(cmd) 67 | out = shell(cmd) 68 | logging.info(out) 69 | 70 | 71 | def run(): 72 | _, triggered_host, trigger_id, event_id, *__ = sys.argv 73 | 74 | logging.info( 75 | "Getting Started with the event: {}/tr_events.php?triggerid={}&eventid={}".format( 76 | config.zbx_url, trigger_id, event_id 77 | ) 78 | ) 79 | 80 | event = zapi.event.get( 81 | eventids=event_id, 82 | select_acknowledges=["username", "action"], 83 | output=["username", "action"], 84 | ) 85 | 86 | if not event: 87 | logging.error("Event {} not found".format(event_id)) 88 | exit(1) 89 | 90 | acknowledges = event[0]["acknowledges"][0] 91 | 92 | ack_alias = acknowledges["username"] 93 | ack_action = acknowledges["action"] 94 | 95 | if ack_alias not in config.acknowledge_users: 96 | logging.info( 97 | "Not trusted user in acknowledge: {}. Skipping this request to fix".format( 98 | ack_alias 99 | ) 100 | ) 101 | exit(0) 102 | 103 | trigger = zapi.trigger.get(triggerids=trigger_id, output="extend")[0] 104 | trigger_description = trigger["description"] 105 | trigger_comment = trigger["comments"] 106 | 107 | if ack_action == "1": 108 | logging.info( 109 | 'The "{}" trigger was manually closed by the "{}" user. No further action required'.format( 110 | trigger_description, ack_alias 111 | ) 112 | ) 113 | exit(0) 114 | 115 | if triggered_host == config.hosts_host: 116 | hostname = trigger_description[trigger_description.rfind(" = ") + 3 :] 117 | fix = trigger_comment[trigger_comment.rfind("\r\n\r\n") + 4 :] 118 | do_fix(hostname, fix) 119 | elif triggered_host == config.packages_host: 120 | _, hosts, __, fix = filter(lambda x: x.strip(), trigger_comment.split("\r\n")) 121 | hosts = hosts.splitlines() 122 | hosts_cnt = len(hosts) 123 | for idx, hostname in enumerate(hosts, 1): 124 | logging.info( 125 | "[{idx} of {hosts_cnt}] {hostname}".format( 126 | idx=idx, hosts_cnt=hosts_cnt, hostname=hostname 127 | ) 128 | ) 129 | do_fix(hostname, fix) 130 | else: 131 | logging.info( 132 | "Host {} that triggered the trigger does not match the required: {} or {}".format( 133 | triggered_host, config.packages_host, config.hosts_host 134 | ) 135 | ) 136 | 137 | 138 | if __name__ == "__main__": 139 | 140 | logging.basicConfig( 141 | level=logging.INFO, 142 | filename=config.log_file, 143 | format="%(asctime)s %(process)d %(levelname)s %(message)s [%(filename)s:%(lineno)d]", 144 | ) 145 | 146 | zapi = ZabbixAPI(config.zbx_url, timeout=10) 147 | zapi.session.verify = config.zbx_verify_ssl 148 | zapi.login(config.zbx_user, config.zbx_pwd) 149 | logging.info("Connected to Zabbix API v.{}".format(zapi.api_version())) 150 | run() 151 | logging.info("End") 152 | -------------------------------------------------------------------------------- /media/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulnersCom/zabbix-threat-control/11ab3f127fb887ffe796a29a1be4ddb297b5b378/media/dashboard.png -------------------------------------------------------------------------------- /media/hosts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulnersCom/zabbix-threat-control/11ab3f127fb887ffe796a29a1be4ddb297b5b378/media/hosts.gif -------------------------------------------------------------------------------- /media/packages.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulnersCom/zabbix-threat-control/11ab3f127fb887ffe796a29a1be4ddb297b5b378/media/packages.gif -------------------------------------------------------------------------------- /os-report/report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import inspect 5 | import pkgutil 6 | import scan_modules 7 | 8 | 9 | class ScannerEngine: 10 | def __init__(self, ssh_prefix=None): 11 | self.os_instance_classes = self.get_instance_classes() 12 | self.instance = self.__get_instance(ssh_prefix) 13 | 14 | @staticmethod 15 | def get_instance_classes(): 16 | members = set() 17 | for module_path, module_name, is_pkg in pkgutil.iter_modules(scan_modules.__path__): 18 | members = members.union( 19 | inspect.getmembers( 20 | __import__('%s.%s' % ('scan_modules', module_name), fromlist=['scan_modules']), 21 | lambda member: ( 22 | inspect.isclass(member) and 23 | issubclass(member, scan_modules.os_detect.ScannerInterface) and 24 | member.__module__ == '%s.%s' % ('scan_modules', module_name) and 25 | member != scan_modules.os_detect.ScannerInterface 26 | ) 27 | ) 28 | ) 29 | 30 | return members 31 | 32 | def __get_instance(self, ssh_prefix): 33 | inited = [instance[1](ssh_prefix) for instance in self.os_instance_classes] 34 | if not inited: 35 | raise Exception("No OS Detection classes found") 36 | os_instance = max(inited, key=lambda x: x.os_detection_weight) 37 | if os_instance.os_detection_weight: 38 | return os_instance 39 | 40 | def audit_system(self): 41 | if len(sys.argv) < 2: 42 | return self.instance 43 | elif sys.argv[1] == 'os': 44 | print(self.instance.os_family) 45 | elif sys.argv[1] == 'version': 46 | print(self.instance.os_version) 47 | elif sys.argv[1] == 'package': 48 | print(self.instance.get_pkg()) 49 | return self.instance 50 | 51 | 52 | if __name__ == "__main__": 53 | ScannerEngine().audit_system() 54 | -------------------------------------------------------------------------------- /os-report/scan_modules/__init__.py: -------------------------------------------------------------------------------- 1 | import glob 2 | from os.path import dirname, basename, isfile 3 | 4 | 5 | modules = glob.glob(dirname(__file__) + "/*.py") 6 | __all__ = [basename(f)[:-3] for f in modules if isfile(f)] 7 | -------------------------------------------------------------------------------- /os-report/scan_modules/centos_detect.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from scan_modules.linux_detect import LinuxDetect 4 | 5 | 6 | class RpmBasedDetect(LinuxDetect): 7 | supported_families = ( 8 | "redhat", 9 | "centos", 10 | "oraclelinux", 11 | "suse", 12 | "fedora", 13 | "ol", 14 | "rhel", 15 | "opensuse", 16 | "sles", 17 | "redos" 18 | ) 19 | 20 | def __init__(self, ssh_prefix): 21 | super(RpmBasedDetect, self).__init__(ssh_prefix) 22 | 23 | def os_detect(self): 24 | os_detection = super(RpmBasedDetect, self).os_detect() 25 | if os_detection: 26 | os_version, os_family, os_detection_weight = os_detection 27 | 28 | if os_family in self.supported_families: 29 | os_detection_weight = 60 30 | return os_version, os_family, os_detection_weight 31 | 32 | version = self.execute_cmd("cat /etc/redos-release") 33 | if version: 34 | os_version = re.search(r"\s+\(?(\d+)\.", version).group(1) 35 | os_family = "redos" 36 | os_detection_weight = 60 37 | return os_version, os_family, os_detection_weight 38 | 39 | version = self.execute_cmd("cat /etc/centos-release") 40 | if version: 41 | os_version = re.search(r"\s+\(?(\d+)\.", version).group(1) 42 | os_family = "centos" 43 | os_detection_weight = 70 44 | return os_version, os_family, os_detection_weight 45 | 46 | version = self.execute_cmd("cat /etc/redhat-release") 47 | if version: 48 | os_version = re.search(r"\s+(\d+)\.", version).group(1) 49 | os_family = "rhel" 50 | os_detection_weight = 60 51 | return os_version, os_family, os_detection_weight 52 | 53 | version = self.execute_cmd("cat /etc/SuSE-release") 54 | if version: 55 | os_version = re.search(r"VERSION = (\d+)", version).group(1) 56 | os_family = "opensuse" 57 | os_detection_weight = 70 58 | return os_version, os_family, os_detection_weight 59 | 60 | def get_pkg(self): 61 | pkg_list = self.execute_cmd("rpm -qa | grep -v '^kernel-'") 62 | uname = self.execute_cmd("uname -r") 63 | pkg_list += self.execute_cmd("rpm -qa |grep '^kernel.*" + uname + "'") 64 | return pkg_list 65 | -------------------------------------------------------------------------------- /os-report/scan_modules/debian_detect.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from scan_modules.linux_detect import LinuxDetect 4 | 5 | 6 | class DebianBasedDetect(LinuxDetect): 7 | deb_code_map = { 8 | "stretch": "9", 9 | "jessie": "8", 10 | "wheezy": "7", 11 | "squeeze": "6", 12 | "lenny": "5", 13 | "etch": "4", 14 | "sarge": "3.1", 15 | "woody": "3.0", 16 | "potato": "2.2", 17 | "slink": "2.1", 18 | "hamm": "2.0", 19 | } 20 | supported_families = ("debian", "ubuntu", "kali") 21 | 22 | def __init__(self, ssh_prefix): 23 | super(DebianBasedDetect, self).__init__(ssh_prefix) 24 | 25 | def os_detect(self): 26 | os_detection = super(DebianBasedDetect, self).os_detect() 27 | if os_detection: 28 | os_version, os_family, os_detection_weight = os_detection 29 | 30 | if os_family in self.supported_families: 31 | os_detection_weight = 60 32 | return os_version, os_family, os_detection_weight 33 | 34 | version = self.execute_cmd("cat /etc/debian_version") 35 | if version and re.match(r"^[\d\.]+$", version): 36 | os_version = version 37 | os_family = "debian" 38 | os_detection_weight = 60 39 | return os_version, os_family, os_detection_weight 40 | elif version and re.match(r"^\w+/\w+", version): 41 | os_code = re.search(r"^(\w+)/", version).group(1).lower() 42 | if os_code in self.deb_code_map: 43 | os_version = self.deb_code_map[os_code] 44 | os_family = "debian" 45 | os_detection_weight = 60 46 | return os_version, os_family, os_detection_weight 47 | 48 | version = self.execute_cmd("cat /etc/lsb-release") 49 | if version: 50 | mID = re.search('^DISTRIB_ID="?(.*?)"?', version, re.MULTILINE) 51 | mVer = re.search('^DISTRIB_RELEASE="?(.*?)"?', version, re.MULTILINE) 52 | if mID and mVer: 53 | os_family = mID.group(1).lower() 54 | os_version = mVer.group(1).lower() 55 | os_detection_weight = 60 56 | return os_version, os_family, os_detection_weight 57 | 58 | def get_pkg(self): 59 | return self.execute_cmd( 60 | "dpkg-query -W -f='${Status} ${Package} ${Version} ${Architecture}\\n'| " 61 | 'awk \'($1 == "install" || $1 == "hold") && ($2 == "ok") {print $4" "$5" "$6}\'' 62 | ) 63 | -------------------------------------------------------------------------------- /os-report/scan_modules/linux_detect.py: -------------------------------------------------------------------------------- 1 | import re 2 | from scan_modules.nix_detect import NixDetect 3 | 4 | 5 | class LinuxDetect(NixDetect): 6 | def os_detect(self): 7 | version = self.execute_cmd("cat /etc/os-release") 8 | if version: 9 | re_family = re.search(r"^ID=(.*)", version, re.MULTILINE) 10 | if re_family: 11 | os_family = re_family.group(1).lower().strip('"') 12 | else: 13 | return 14 | 15 | reVersion = re.search("^VERSION_ID=(.*)", version, re.MULTILINE) 16 | if reVersion: 17 | os_version = reVersion.group(1).lower().strip('"') 18 | else: 19 | return 20 | 21 | os_detection_weight = 50 22 | return os_version, os_family, os_detection_weight 23 | -------------------------------------------------------------------------------- /os-report/scan_modules/nix_detect.py: -------------------------------------------------------------------------------- 1 | from scan_modules.os_detect import ScannerInterface 2 | 3 | 4 | class NixDetect(ScannerInterface): 5 | def os_detect(self): 6 | os_family = self.execute_cmd("uname -s") 7 | os_version = self.execute_cmd("uname -r") 8 | if os_family and os_version: 9 | os_detection_weight = 10 10 | return os_version, os_family, os_detection_weight 11 | 12 | def get_host_name(self): 13 | return self.execute_cmd("hostname") 14 | 15 | def get_ip(self): 16 | return self.execute_cmd( 17 | "ifconfig | " 18 | "grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | " 19 | "grep -Eo '([0-9]*\.){3}[0-9]*' | " 20 | "grep -v '127.0.0.1' | " 21 | "head -1" 22 | ) 23 | -------------------------------------------------------------------------------- /os-report/scan_modules/os_detect.py: -------------------------------------------------------------------------------- 1 | import re 2 | import uuid 3 | import subprocess 4 | 5 | 6 | try: 7 | from subprocess import DEVNULL # py3k 8 | except ImportError: 9 | import os 10 | DEVNULL = open(os.devnull, 'wb') 11 | 12 | 13 | class ScannerInterface: 14 | def __init__(self, ssh_prefix): 15 | self.os_version = None 16 | self.os_family = None 17 | self.os_detection_weight = 0 18 | self.ssh_prefix = ssh_prefix 19 | os_detection = self.os_detect() 20 | if os_detection is not None: 21 | self.os_version, self.os_family, self.os_detection_weight = os_detection 22 | 23 | def execute_cmd(self, command): 24 | if self.ssh_prefix: 25 | command = "%s %s" % (self.ssh_prefix, command) 26 | randPre = str(uuid.uuid4()).split('-')[0] 27 | randAfter = str(uuid.uuid4()).split('-')[0] 28 | randFail = str(uuid.uuid4()).split('-')[0] 29 | command = "echo %s; %s; echo %s || echo %s" % (randPre, command, randAfter, randFail) 30 | cmdResult = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=DEVNULL, shell=True).communicate()[0] 31 | 32 | if isinstance(cmdResult, bytes): 33 | cmdResult = cmdResult.decode('utf8') 34 | if randFail in cmdResult: 35 | return None 36 | else: 37 | resMatch = re.search(r"%s\n(.*)\n%s" % (randPre, randAfter), cmdResult, re.DOTALL) 38 | if resMatch: 39 | return resMatch.group(1) 40 | else: 41 | return "" 42 | 43 | def os_detect(self): 44 | raise NotImplementedError 45 | 46 | def get_pkg(self): 47 | raise NotImplementedError 48 | -------------------------------------------------------------------------------- /prepare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Zabbix vulnerability assessment plugin. 5 | 6 | Script will create these objects in Zabbix using the API: 7 | - A template; through which data will be collected from the servers. 8 | - Zabbix hosts; for obtaining data on vulnerabilities. 9 | - Dashboard; for their display. 10 | - Action; for run the command fixes the vulnerability. 11 | """ 12 | 13 | __version__ = "2.2" 14 | 15 | 16 | import sys 17 | import argparse 18 | import subprocess 19 | import config 20 | from time import sleep 21 | from random import randint 22 | from datetime import datetime, timedelta 23 | from pyzabbix import ZabbixAPI 24 | 25 | 26 | MEDIAN_GRAPH_NAME = "Median CVSS Score" 27 | SCORE_GRAPH_NAME = "CVSS Score ratio by servers" 28 | COLORS = [ 29 | "DD0000", 30 | "EE0000", 31 | "FF3333", 32 | "EEEE00", 33 | "FFFF66", 34 | "00EEEE", 35 | "00DDDD", 36 | "3333FF", 37 | "6666FF", 38 | "00DD00", 39 | "33FF33", 40 | ] 41 | 42 | timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 43 | host_start_time = datetime.now() + timedelta(minutes=randint(60, 1380)) 44 | ztc_start_time = host_start_time + timedelta(minutes=10) 45 | delay_report = host_start_time.strftime("0;wd1-7h%Hm%M") 46 | delay_ztc = ztc_start_time.strftime("0;wd1-7h%Hm%M") 47 | 48 | min_zapi_version = 5.0 49 | 50 | 51 | def check_zabbix_utils(check_type, host_conn): 52 | output, exitcode, command = 1, 1, "" 53 | if check_type == "agent": 54 | check_key = "CheckRemoteCommand" 55 | command = ( 56 | "{zabbix_get_bin} " 57 | "-s {host_conn} " 58 | '-k system.run["echo {check_key}"]'.format( 59 | zabbix_get_bin=config.zabbix_get_bin, 60 | host_conn=host_conn, 61 | check_key=check_key, 62 | ) 63 | ) 64 | elif check_type == "server": 65 | check_key = '"response":"success"' 66 | command = ( 67 | "{zabbix_sender_bin} " 68 | "-z {host_conn} " 69 | "-p {port} " 70 | "-s zabbix_sender_ztc_test " 71 | "-k zabbix_sender_ztc_test " 72 | "-o 1 " 73 | "-vv".format( 74 | zabbix_sender_bin=config.zabbix_sender_bin, 75 | host_conn=host_conn, 76 | port=config.zbx_server_port, 77 | check_key=check_key, 78 | ) 79 | ) 80 | else: 81 | return False, output, command 82 | 83 | proc = subprocess.Popen( 84 | command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True 85 | ) 86 | output = proc.communicate()[0].decode("utf8") 87 | if output.find(check_key) != -1: 88 | return True, output, command 89 | else: 90 | return False, output, command 91 | 92 | 93 | def create_zbx_host( 94 | host, 95 | name, 96 | group_id, 97 | app_name, 98 | lld_name, 99 | lld_key, 100 | item_proto_name, 101 | item_proto_key, 102 | trig_proto_expression, 103 | trig_proto_desc, 104 | trig_proto_url, 105 | trig_proto_comment, 106 | ): 107 | 108 | host_id = get_zabbix_obj("host", {"host": host, "name": name}, "hostid") 109 | if host_id: 110 | host_id = host_id[0]["hostid"] 111 | bkp_host = host + ".bkp-" + timestamp 112 | bkp_name = name + ".bkp-" + timestamp 113 | zapi.host.update(hostid=host_id, host=bkp_host, name=bkp_name, status=1) 114 | print( 115 | 'Host "{}" (id: {}) was renamed to "{}" and deactivated.'.format( 116 | name, host_id, bkp_name 117 | ) 118 | ) 119 | 120 | host_id = zapi.host.create( 121 | host=host, 122 | name=name, 123 | groups=[{"groupid": group_id}], 124 | tags=[{"tag": "vulners", "value": app_name}], 125 | macros=[{"macro": "{$SCORE.MIN}", "value": str(config.min_cvss)}], 126 | interfaces=[ 127 | { 128 | "type": 1, 129 | "main": 1, 130 | "useip": host_use_ip, 131 | "ip": "127.0.0.1", 132 | "dns": config.zbx_server_fqdn, 133 | "port": "10050", 134 | } 135 | ], 136 | )["hostids"][0] 137 | 138 | lld_id = zapi.discoveryrule.create( 139 | type=2, 140 | hostid=host_id, 141 | name=lld_name, 142 | key_=lld_key, 143 | trapper_hosts="", 144 | lifetime="0", 145 | **({"value_type": "4", "units": ""} if zbx_version <= 7.0 else {}), 146 | )["itemids"][0] 147 | 148 | zapi.itemprototype.create( 149 | { 150 | "hostid": host_id, 151 | "ruleid": lld_id, 152 | "name": item_proto_name, 153 | "key_": item_proto_key, 154 | "delay": "0", 155 | "status": "0", 156 | "type": "2", 157 | "value_type": "0", 158 | "trapper_hosts": "", 159 | "units": "", 160 | "interfaceid": "0", 161 | } 162 | ) 163 | 164 | zapi.triggerprototype.create( 165 | expression=trig_proto_expression, 166 | description=trig_proto_desc, 167 | url=trig_proto_url, 168 | manual_close=1, 169 | priority="0", 170 | comments=trig_proto_comment, 171 | status="0", 172 | ) 173 | print('Created host "{}" (id: {})\n'.format(name, host_id)) 174 | return host_id 175 | 176 | 177 | def check_utils(): 178 | print("Checking the connection to the zabbix-agent...") 179 | use_ip = True 180 | successful, out, cmd = check_zabbix_utils("agent", "127.0.0.1") 181 | if successful: 182 | print( 183 | 'Сompleted successfully. For connecting with zabbix-agent used address "127.0.0.1"\n' 184 | ) 185 | else: 186 | successful, out, cmd = check_zabbix_utils("agent", config.zbx_server_fqdn) 187 | if successful: 188 | use_ip = False 189 | print( 190 | 'For connecting with zabbix-agent used address "{}"\n'.format( 191 | config.zbx_server_fqdn 192 | ) 193 | ) 194 | else: 195 | print( 196 | "Error: Can't execute remote command on zabbix-agent:\n" 197 | "Command: {}\n{}\nPlease fix this for continue!".format(cmd, out) 198 | ) 199 | exit(1) 200 | 201 | print("Checking the connection to the zabbix-server via zabbix_sender...") 202 | successful, out, cmd = check_zabbix_utils("server", config.zbx_server_fqdn) 203 | if successful: 204 | print( 205 | "Сompleted successfully. " 206 | 'For connecting with zabbix-server used address "{}"\n'.format( 207 | config.zbx_server_fqdn 208 | ) 209 | ) 210 | else: 211 | print( 212 | "Error: Can't send data with zabbix-sender:\n" 213 | "Command: {}\n{}\n\nPlease fix this for continue!".format(cmd, out) 214 | ) 215 | exit(1) 216 | return int(use_ip) 217 | 218 | 219 | def get_zabbix_obj(obj_type, filter_query, id_key, id_only=False): 220 | zbx_obj = getattr(zapi, obj_type).get(filter=filter_query, output=[id_key]) 221 | if id_only: 222 | return zbx_obj[-1][id_key] 223 | return zbx_obj 224 | 225 | 226 | def create_hosts(): 227 | host_group_id = get_zabbix_obj("hostgroup", {"name": config.group_name}, "groupid") 228 | if not host_group_id: 229 | print('Created host group "{}"\n'.format(config.group_name)) 230 | host_group_id = zapi.hostgroup.create(name=config.group_name)["groupids"][0] 231 | sleep(5) # wait until group created 232 | else: 233 | print( 234 | 'Host group "{}" already exists. Use this group\n'.format(config.group_name) 235 | ) 236 | host_group_id = host_group_id[0]["groupid"] 237 | 238 | if zbx_version < 5.4: 239 | expression = "{{{zabbix_host}:{zabbix_host}[{{{id}}}].last()}} > 0 and {{{score}}} >= {{$SCORE.MIN}}" 240 | else: 241 | expression = "last(/{zabbix_host}/{zabbix_host}[{{{id}}}]) > 0 and {{{score}}} >= {{$SCORE.MIN}}" 242 | 243 | create_zbx_host( 244 | host=config.hosts_host, 245 | name=config.hosts_name, 246 | group_id=host_group_id, 247 | app_name=config.application_name, 248 | lld_name="Hosts", 249 | lld_key="vulners.hosts_lld", 250 | item_proto_name="CVSS Score on {#H.HOST} [{#H.VNAME}]", 251 | item_proto_key="vulners.hosts[{#H.ID}]", 252 | trig_proto_expression=expression.format( 253 | zabbix_host=config.hosts_host, id="#H.ID", score="#H.SCORE" 254 | ), 255 | trig_proto_desc="Score {#H.SCORE}. Host = {#H.VNAME}", 256 | trig_proto_url="", 257 | trig_proto_comment="Cumulative fix:\r\n\r\n{#H.FIX}", 258 | ) 259 | 260 | create_zbx_host( 261 | host=config.bulletins_host, 262 | name=config.bulletins_name, 263 | group_id=host_group_id, 264 | app_name=config.application_name, 265 | lld_name="Bulletins", 266 | lld_key="vulners.bulletins_lld", 267 | item_proto_name="[{#BULLETIN.SCORE}] [{#BULLETIN.ID}] - affected hosts", 268 | item_proto_key="vulners.bulletins[{#BULLETIN.ID}]", 269 | trig_proto_expression=expression.format( 270 | zabbix_host=config.bulletins_host, 271 | id="#BULLETIN.ID", 272 | score="#BULLETIN.SCORE", 273 | ), 274 | trig_proto_desc="Impact {#BULLETIN.IMPACT}. Score {#BULLETIN.SCORE}. Affected {ITEM.VALUE}. Bulletin = {#BULLETIN.ID}", 275 | trig_proto_url="https://vulners.com/info/{#BULLETIN.ID}", 276 | trig_proto_comment="Vulnerabilities are found on:\r\n\r\n{#BULLETIN.HOSTS}", 277 | ) 278 | 279 | create_zbx_host( 280 | host=config.packages_host, 281 | name=config.packages_name, 282 | group_id=host_group_id, 283 | app_name=config.application_name, 284 | lld_name="Packages", 285 | lld_key="vulners.packages_lld", 286 | item_proto_name="[{#PKG.SCORE}] [{#PKG.ID}] - affected hosts", 287 | item_proto_key="vulners.packages[{#PKG.ID}]", 288 | trig_proto_expression=expression.format( 289 | zabbix_host=config.packages_host, id="#PKG.ID", score="#PKG.SCORE" 290 | ), 291 | trig_proto_desc="Impact {#PKG.IMPACT}. Score {#PKG.SCORE}. Affected {ITEM.VALUE}. Package = {#PKG.ID}", 292 | trig_proto_url="https://vulners.com/info/{#PKG.URL}", 293 | trig_proto_comment="Vulnerabilities are found on:\r\n\r\n{#PKG.HOSTS}\r\n----\r\n{#PKG.FIX}", 294 | ) 295 | 296 | statistics_host_id = zapi.host.get( 297 | filter={"host": config.statistics_host, "name": config.statistics_name}, 298 | output=["hostid"], 299 | ) 300 | if statistics_host_id: 301 | statistics_host_id = statistics_host_id[0]["hostid"] 302 | bkp_h_stats = config.statistics_host + ".bkp-" + timestamp 303 | bkp_h_stats_vname = config.statistics_name + ".bkp-" + timestamp 304 | 305 | zapi.host.update( 306 | hostid=statistics_host_id, 307 | host=bkp_h_stats, 308 | name=bkp_h_stats_vname, 309 | status=1, 310 | ) 311 | 312 | print( 313 | 'Host "{}" (id: {}) was renamed to "{}" and deactivated'.format( 314 | config.statistics_name, statistics_host_id, bkp_h_stats_vname 315 | ) 316 | ) 317 | 318 | statistics_host_id = zapi.host.create( 319 | host=config.statistics_host, 320 | name=config.statistics_name, 321 | groups=[{"groupid": host_group_id}], 322 | macros=[ 323 | {"macro": config.stats_macros_name, "value": config.stats_macros_value} 324 | ], 325 | interfaces=[ 326 | { 327 | "type": 1, 328 | "main": 1, 329 | "useip": host_use_ip, 330 | "ip": "127.0.0.1", 331 | "dns": config.zbx_server_fqdn, 332 | "port": "10050", 333 | } 334 | ], 335 | )["hostids"][0] 336 | 337 | host_interface_id = zapi.hostinterface.get( 338 | hostids=statistics_host_id, output="interfaceid" 339 | )[0]["interfaceid"] 340 | 341 | zapi.item.create( 342 | name="Service item for running {$WORK_SCRIPT_CMD}", 343 | key_="system.run[{$WORK_SCRIPT_CMD},nowait]", 344 | hostid=statistics_host_id, 345 | type=0, 346 | value_type=3, 347 | interfaceid=host_interface_id, 348 | tags=[{"tag": "vulners", "value": config.application_name}], 349 | delay=delay_ztc, 350 | ) 351 | 352 | zapi.item.create( 353 | *( 354 | { 355 | "name": "CVSS Score - %s" % name, 356 | "key_": "vulners.%s" % name.replace(" ", ""), 357 | "hostid": statistics_host_id, 358 | "type": "2", 359 | "value_type": "3", 360 | "trapper_hosts": "", 361 | "tags": [{"tag": "vulners", "value": config.application_name}], 362 | } 363 | for name in ("Total Hosts", "Maximum", "Average", "Minimum") 364 | ) 365 | ) 366 | 367 | median_item_id = zapi.item.create( 368 | { 369 | "name": "CVSS Score - Median", 370 | "key_": "vulners.scoreMedian", 371 | "hostid": statistics_host_id, 372 | "type": "2", 373 | "value_type": "0", 374 | "trapper_hosts": "", 375 | "tags": [{"tag": "vulners", "value": config.application_name}], 376 | } 377 | )["itemids"][0] 378 | 379 | hosts_cnt_score_item_ids = zapi.item.create( 380 | *( 381 | { 382 | "name": "CVSS Score - Hosts with a score ~ %s" % idx, 383 | "key_": "vulners.hostsCountScore%s" % idx, 384 | "hostid": statistics_host_id, 385 | "type": "2", 386 | "value_type": "3", 387 | "trapper_hosts": "", 388 | "tags": [{"tag": "vulners", "value": config.application_name}], 389 | } 390 | for idx in range(11) 391 | ) 392 | )["itemids"] 393 | 394 | zapi.graph.create( 395 | { 396 | "hostids": statistics_host_id, 397 | "name": MEDIAN_GRAPH_NAME, 398 | "width": "1000", 399 | "height": "300", 400 | "show_work_period": "0", 401 | "graphtype": "0", 402 | "show_legend": "0", 403 | "show_3d": "0", 404 | "gitems": [{"itemid": median_item_id, "color": "00AAAA", "drawtype": "5"}], 405 | } 406 | ) 407 | 408 | gitems = [] 409 | 410 | for idx, item_id in enumerate(hosts_cnt_score_item_ids): 411 | gitems.append( 412 | { 413 | "itemid": item_id, 414 | "color": COLORS[idx], 415 | "drawtype": "5", 416 | "calc_fnc": "9", 417 | } 418 | ) 419 | 420 | zapi.graph.create( 421 | { 422 | "hostids": statistics_host_id, 423 | "name": SCORE_GRAPH_NAME, 424 | "width": "1000", 425 | "height": "300", 426 | "show_work_period": "0", 427 | "graphtype": "2", 428 | "show_legend": "0", 429 | "show_3d": "1", 430 | "gitems": gitems, 431 | } 432 | ) 433 | 434 | print('Created host "{}"\n'.format(config.statistics_name)) 435 | 436 | 437 | def create_dashboard(): 438 | host_id = get_zabbix_obj( 439 | "host", {"host": config.hosts_host, "name": config.hosts_name}, "hostid", True 440 | ) 441 | bulletins_host_id = get_zabbix_obj( 442 | "host", 443 | {"host": config.bulletins_host, "name": config.bulletins_name}, 444 | "hostid", 445 | True, 446 | ) 447 | packages_host_id = get_zabbix_obj( 448 | "host", 449 | {"host": config.packages_host, "name": config.packages_name}, 450 | "hostid", 451 | True, 452 | ) 453 | median_graph_id = get_zabbix_obj( 454 | "graph", {"name": MEDIAN_GRAPH_NAME}, "graphid", True 455 | ) 456 | score_graph_id = get_zabbix_obj( 457 | "graph", {"name": SCORE_GRAPH_NAME}, "graphid", True 458 | ) 459 | 460 | widgets = [ 461 | { 462 | "type": "problems", 463 | "name": config.bulletins_name, 464 | "x": "8", 465 | "y": "8", 466 | "width": "8", 467 | "height": "8", 468 | "fields": [ 469 | {"type": "0", "name": "rf_rate", "value": "900"}, 470 | {"type": "0", "name": "show", "value": "3"}, 471 | {"type": "0", "name": "show_lines", "value": "100"}, 472 | {"type": "0", "name": "sort_triggers", "value": "16"}, 473 | {"type": "3", "name": "hostids", "value": bulletins_host_id}, 474 | ], 475 | }, 476 | { 477 | "type": "problems", 478 | "name": config.packages_name, 479 | "x": "8", 480 | "y": "0", 481 | "width": "8", 482 | "height": "8", 483 | "fields": [ 484 | {"type": "0", "name": "rf_rate", "value": "600"}, 485 | {"type": "0", "name": "show", "value": "3"}, 486 | {"type": "0", "name": "show_lines", "value": "100"}, 487 | {"type": "0", "name": "sort_triggers", "value": "16"}, 488 | {"type": "3", "name": "hostids", "value": packages_host_id}, 489 | ], 490 | }, 491 | { 492 | "type": "problems", 493 | "name": config.hosts_name, 494 | "x": "0", 495 | "y": "8", 496 | "width": "8", 497 | "height": "8", 498 | "fields": [ 499 | {"type": "0", "name": "rf_rate", "value": "600"}, 500 | {"type": "0", "name": "show", "value": "3"}, 501 | {"type": "0", "name": "show_lines", "value": "100"}, 502 | {"type": "0", "name": "sort_triggers", "value": "16"}, 503 | {"type": "3", "name": "hostids", "value": host_id}, 504 | ], 505 | }, 506 | { 507 | "type": "graph", 508 | "name": MEDIAN_GRAPH_NAME, 509 | "x": "0", 510 | "y": "4", 511 | "width": "8", 512 | "height": "4", 513 | "fields": [ 514 | {"type": "0", "name": "rf_rate", "value": "600"}, 515 | {"type": "0", "name": "show_legend", "value": "0"}, 516 | {"type": "6", "name": "graphid", "value": median_graph_id}, 517 | ], 518 | }, 519 | { 520 | "type": "graph", 521 | "name": SCORE_GRAPH_NAME, 522 | "x": "0", 523 | "y": "0", 524 | "width": "8", 525 | "height": "4", 526 | "fields": [ 527 | {"type": "0", "name": "rf_rate", "value": "600"}, 528 | {"type": "0", "name": "show_legend", "value": "0"}, 529 | {"type": "6", "name": "graphid", "value": score_graph_id}, 530 | ], 531 | }, 532 | ] 533 | 534 | dash_id = zapi.dashboard.get( 535 | filter={"name": config.dash_name}, output=["dashboardid"] 536 | ) 537 | if dash_id: 538 | dash_id = dash_id[0]["dashboardid"] 539 | bkp_dash_name = config.dash_name + "_bkp_" + timestamp 540 | zapi.dashboard.update(dashboardid=dash_id, name=bkp_dash_name) 541 | print( 542 | "Dashboard {} (id: {}) was renamed to {}".format( 543 | config.dash_name, dash_id, bkp_dash_name 544 | ) 545 | ) 546 | 547 | dash_id = zapi.dashboard.create( 548 | name=config.dash_name, 549 | userGroups=[], 550 | users=[], 551 | private=0, 552 | **( 553 | {"pages": [{"widgets": widgets}]} 554 | if zbx_version > 5.0 555 | else {"widgets": widgets} 556 | ), 557 | ) 558 | dash_id = dash_id["dashboardids"][0] 559 | print( 560 | 'Created dashboard "{dash_name}" (id: {dash_id})\n\n' 561 | 'Script "{stats_macros_value}" will be run every day at {time}\n' 562 | 'via the item "Service item..." on the host "{statistics_name}".\n\n' 563 | "Dashboard URL:\n{zbx_url}/zabbix.php?action=dashboard.view&dashboardid={dash_id}&fullscreen=1\n".format( 564 | dash_name=config.dash_name, 565 | dash_id=dash_id, 566 | stats_macros_value=config.stats_macros_value, 567 | time=ztc_start_time.strftime("%H:%M"), 568 | statistics_name=config.statistics_name, 569 | zbx_url=config.zbx_url, 570 | ) 571 | ) 572 | 573 | 574 | def create_template(): 575 | template_id = zapi.template.get( 576 | filter={"host": config.template_host, "name": config.template_name}, 577 | output=["templateid"], 578 | ) 579 | if template_id: 580 | template_id = template_id[0]["templateid"] 581 | bkp_template_host = config.template_host + ".bkp-" + timestamp 582 | bkp_template_name = config.template_name + ".bkp-" + timestamp 583 | zapi.template.update( 584 | templateid=template_id, host=bkp_template_host, name=bkp_template_name 585 | ) 586 | print( 587 | 'Template "{}" (id: {}) was renamed to "{}"'.format( 588 | config.template_name, template_id, bkp_template_name 589 | ) 590 | ) 591 | if zbx_version >= 6.2: 592 | template_group_id = zapi.templategroup.get( 593 | filter={"name": config.template_group_name}, output=["groupid"] 594 | ) 595 | else: 596 | template_group_id = zapi.hostgroup.get( 597 | filter={"name": config.template_group_name}, output=["groupid"] 598 | ) 599 | template_group_id = template_group_id[0]["groupid"] 600 | 601 | template_id = zapi.template.create( 602 | groups={"groupid": template_group_id}, 603 | macros=[ 604 | { 605 | "macro": config.template_macros_name, 606 | "value": config.template_macros_value, 607 | } 608 | ], 609 | host=config.template_host, 610 | name=config.template_name, 611 | )["templateids"][0] 612 | 613 | for name, arg, value_type in ( 614 | ("Name", "os", 1), 615 | ("Version", "version", 1), 616 | ("Packages", "package", 4), 617 | ): 618 | zapi.item.create( 619 | name="OS - " + name, 620 | key_="system.run[{$REPORT_SCRIPT_PATH} %s]" % arg, 621 | hostid=template_id, 622 | type=0, 623 | value_type=value_type, 624 | interfaceid="0", 625 | tags=[{"tag": "vulners", "value": config.template_application_name}], 626 | delay=delay_report, 627 | ) 628 | 629 | print('Created template "{}" (id: {})\n'.format(config.template_name, template_id)) 630 | 631 | 632 | if __name__ == "__main__": 633 | parser = argparse.ArgumentParser( 634 | description='Creates objects in ZABBIX for "ZTC ". Usage: ./prepare.py -uvtd' 635 | ) 636 | 637 | parser.add_argument( 638 | "-u", 639 | "--utils", 640 | help="check zabbix-sender and zabbix-get settings", 641 | action="store_true", 642 | ) 643 | 644 | parser.add_argument( 645 | "-v", 646 | "--vhosts", 647 | help="create the Virtual ZTC hosts in zabbix", 648 | action="store_true", 649 | ) 650 | 651 | parser.add_argument( 652 | "-t", 653 | "--template", 654 | help="create the ZTC Template in zabbix", 655 | action="store_true", 656 | ) 657 | 658 | parser.add_argument( 659 | "-d", 660 | "--dashboard", 661 | help="create the ZTC Dashboard in zabbix", 662 | action="store_true", 663 | ) 664 | 665 | args = parser.parse_args() 666 | 667 | if not len(sys.argv) > 1: 668 | print( 669 | "\nYou do not specify the objects that you want to create.\n" 670 | "Show help: ./prepare.py -h\n" 671 | "Typical use: ./prepare.py -uvtd" 672 | ) 673 | exit(0) 674 | 675 | zapi = ZabbixAPI(config.zbx_url, timeout=5) 676 | zapi.session.verify = config.zbx_verify_ssl 677 | zapi.login(config.zbx_user, config.zbx_pwd) 678 | zbx_version = zapi.api_version() 679 | 680 | print("Connected to Zabbix API v.{}\n".format(zapi.api_version())) 681 | 682 | zbx_version = float(".".join(zbx_version.split(".")[:2])) 683 | if zbx_version < min_zapi_version: 684 | print("Required Zabbix version {} or higher\nExit.".format(min_zapi_version)) 685 | exit(0) 686 | 687 | host_use_ip = 1 688 | if args.utils: 689 | host_use_ip = check_utils() 690 | 691 | if args.vhosts: 692 | create_hosts() 693 | 694 | if args.template: 695 | create_template() 696 | 697 | if args.dashboard: 698 | create_dashboard() 699 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.27.1 2 | jpath==1.6 3 | vulners==2.0.2 4 | pyzabbix==1.3.1 5 | 6 | 7 | -------------------------------------------------------------------------------- /scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Zabbix vulnerability assessment plugin.""" 4 | 5 | __version__ = "2.2" 6 | 7 | import os 8 | import re 9 | import argparse 10 | import json 11 | import logging 12 | import pickle 13 | import subprocess 14 | import jpath 15 | import vulners 16 | import config 17 | from time import sleep 18 | from pyzabbix import ZabbixAPI 19 | from statistics import mean, median 20 | 21 | 22 | class Scan: 23 | lld_file_path = os.path.join(config.work_dir, "lld.zbx") 24 | data_file_path = os.path.join(config.work_dir, "data.zbx") 25 | hosts_dump_path = os.path.join(config.work_dir, "dump.bin") 26 | 27 | data_file = None 28 | lld_file = None 29 | 30 | hosts = None 31 | total_hosts_cnt = None 32 | 33 | def __init__(self): 34 | logger.info("Scan running") 35 | self.vapi = vulners.VulnersApi(api_key=config.vuln_api_key) 36 | if config.vuln_proxy_host: 37 | self.vapi._server_url = config.vuln_proxy_host 38 | self.zapi = ZabbixAPI(config.zbx_url, timeout=10) 39 | self.zapi.session.verify = config.zbx_verify_ssl 40 | self.zapi.login(config.zbx_user, config.zbx_pwd) 41 | 42 | logger.info("Connected to Zabbix API v.{}".format(self.zapi.api_version())) 43 | 44 | if self.total_hosts_cnt == 0: 45 | logger.info( 46 | "There are no data in the host-matrix for further processing. Exit" 47 | ) 48 | exit() 49 | 50 | @staticmethod 51 | def shell(command): 52 | proc = subprocess.Popen( 53 | command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True 54 | ) 55 | out = proc.communicate()[0].decode("utf8") 56 | return out 57 | 58 | @staticmethod 59 | def uniq_list_of_dicts(list_): 60 | result = [] 61 | for dict_ in list_: 62 | if dict_ not in result: 63 | result.append(dict_) 64 | return result 65 | 66 | @staticmethod 67 | def verify_os_data(os_name, os_version, os_packages, name, *args, **kwargs): 68 | try: 69 | if os_name and (os_version != "0.0") and len(os_packages) > 5 and (not ("report.py" in os_packages)): 70 | return True 71 | except Exception as err: 72 | logger.warning("Excluded {}. Exception: {}".format(name, err)) 73 | return False 74 | logger.info( 75 | "Excluded {}. OS: {}, Version: {}, Packages: {}".format( 76 | name, os_name, os_version, len(os_packages) 77 | ) 78 | ) 79 | return False 80 | 81 | def create_hosts_dump(self): 82 | with open(self.hosts_dump_path, "wb") as file: 83 | pickle.dump(self.hosts, file) 84 | return True 85 | 86 | def read_hosts_dump(self): 87 | with open(self.hosts_dump_path, "rb") as file: 88 | obj = pickle.load(file) 89 | return obj 90 | 91 | def get_or_create_hosts_matrix(self): 92 | if os.path.exists(self.hosts_dump_path): 93 | logger.info( 94 | "Found a dump of the h_matrix in {}. Loading".format( 95 | self.hosts_dump_path 96 | ) 97 | ) 98 | self.hosts = self.read_hosts_dump() 99 | self.create_hosts_matrix() 100 | 101 | def update_with_zabbix_data(self): 102 | logger.info( 103 | "Received from Zabbix {} hosts for processing".format(self.total_hosts_cnt) 104 | ) 105 | logger.info("Receiving extended data about hosts from Zabbix") 106 | 107 | for idx, host in enumerate(self.hosts, 1): 108 | logger.info( 109 | 'processing host {}'.format( 110 | host["name"] 111 | ) 112 | ) 113 | items = self.zapi.item.get( 114 | hostids=host["hostid"], 115 | search={"key_": config.template_macros_name}, 116 | output=["name", "lastvalue"], 117 | ) 118 | for item in items: 119 | name = item["name"].replace("-", "_").replace(" ", "").lower() 120 | host.update({name: item["lastvalue"]}) 121 | 122 | host.update({"os_name": re.sub("^ol$", "oraclelinux", host["os_name"])}) 123 | 124 | logger.info( 125 | '[{} of {}] "{}". Successfully received extended data'.format( 126 | idx, self.total_hosts_cnt, host["name"] 127 | ) 128 | ) 129 | 130 | def update_with_vulners_data(self): 131 | logger.info("Receiving the vulnerabilities from Vulners") 132 | for idx, host in enumerate(self.hosts, 1): 133 | vulnerabilities = self.vapi.os_audit( 134 | os=host["os_name"], 135 | version=host["os_version"], 136 | packages=host["os_packages"].splitlines(), 137 | ) 138 | if vulnerabilities.get("errorCode", 0) == 0: 139 | host.update( 140 | {"vulners_data": {"data": vulnerabilities, "success": True}} 141 | ) 142 | logger.info( 143 | '[{} of {}] "{}". Successfully received data from Vulners'.format( 144 | idx, self.total_hosts_cnt, host["name"] 145 | ) 146 | ) 147 | else: 148 | host.update( 149 | {"vulners_data": {"data": vulnerabilities, "success": False}} 150 | ) 151 | logger.info( 152 | '[{} of {}] "{}". Can\'t receive data from Vulners. Error message: {}'.format( 153 | idx, 154 | self.total_hosts_cnt, 155 | host["name"], 156 | vulnerabilities.get("error", 0) 157 | ) 158 | ) 159 | 160 | def create_hosts_matrix(self): 161 | template_id = self.zapi.template.get(filter={"host": config.template_host})[0][ 162 | "templateid" 163 | ] 164 | if args.limit: 165 | logger.info( 166 | '"limit" option is used. Fetching data from Zabbix is limited to {} hosts.'.format( 167 | args.limit 168 | ) 169 | ) 170 | self.hosts = self.zapi.host.get( 171 | templated_hosts=False, 172 | templateids=template_id, 173 | monitored_hosts=True, 174 | output=["hostid", "host", "name"], 175 | limit=args.limit, 176 | ) 177 | 178 | self.total_hosts_cnt = len(self.hosts) 179 | 180 | self.update_with_zabbix_data() 181 | 182 | logger.info("Exclude invalid response data from Zabbix") 183 | self.hosts = list(filter(lambda host: self.verify_os_data(**host), self.hosts)) 184 | filtered_hosts_cnt = len(self.hosts) 185 | removed_hosts_cnt = self.total_hosts_cnt - filtered_hosts_cnt 186 | logger.info( 187 | "There are {} entries left. Removed: {}".format( 188 | self.total_hosts_cnt, removed_hosts_cnt 189 | ) 190 | ) 191 | 192 | self.update_with_vulners_data() 193 | 194 | logger.info("Exclude invalid response data from Vulners") 195 | self.hosts = list( 196 | filter(lambda host: host["vulners_data"]["success"], self.hosts) 197 | ) 198 | 199 | removed_hosts_cnt = self.total_hosts_cnt - len(self.hosts) 200 | self.total_hosts_cnt = len(self.hosts) 201 | logger.info( 202 | "There are {} entries left. Removed: {}".format( 203 | self.total_hosts_cnt, removed_hosts_cnt 204 | ) 205 | ) 206 | 207 | def write_score_data(self): 208 | logger.info( 209 | "Creating an additional field in the host-matrix based on data from Vulners" 210 | ) 211 | for idx, host in enumerate(self.hosts, 1): 212 | host_bulletins = [] 213 | host_packages = {} 214 | vulners_data = host.pop("vulners_data", {}) 215 | for row in jpath.get_all(jpath="data.packages.*.*.*", data=vulners_data): 216 | package_name = row["package"] 217 | bulletin_id = row["bulletinID"] 218 | score = float(row["cvss"]["score"]) 219 | fix = row["fix"] 220 | 221 | host_bulletins.append({"name": bulletin_id, "score": score}) 222 | 223 | package = host_packages.setdefault(package_name, {}) 224 | if package.get("score") is None or package["score"] < score: 225 | package.update( 226 | { 227 | "name": package_name, 228 | "score": score, 229 | "fix": fix, 230 | "bulletin_id": bulletin_id, 231 | } 232 | ) 233 | 234 | host.update( 235 | { 236 | "cumulative_fix": vulners_data["data"]["cumulativeFix"].replace( 237 | ",", "" 238 | ), 239 | "score": vulners_data["data"]["cvss"]["score"], 240 | "packages": list(host_packages.values()), 241 | "bulletins": self.uniq_list_of_dicts(host_bulletins), 242 | } 243 | ) 244 | 245 | logger.info( 246 | '[{} of {}] "{}". Successfully processed'.format( 247 | idx, self.total_hosts_cnt, host["name"] 248 | ) 249 | ) 250 | 251 | logger.info("Creating an LLD-data: CVSS-Scores and Cumulative-Fix commands") 252 | discovery_hosts = [] 253 | 254 | for idx, host in enumerate(self.hosts, 1): 255 | discovery_hosts.append( 256 | { 257 | "{#H.VNAME}": host["name"], 258 | "{#H.HOST}": host["host"], 259 | "{#H.ID}": host["hostid"], 260 | "{#H.FIX}": host["cumulative_fix"], 261 | "{#H.SCORE}": host["score"], 262 | } 263 | ) 264 | 265 | self.data_file.write( 266 | '"{}" vulners.hosts[{}] {}\n'.format( 267 | config.hosts_host, host["hostid"], host["score"] 268 | ) 269 | ) 270 | 271 | discovery_hosts_json = json.dumps( 272 | {"data": discovery_hosts}, separators=(",", ":") 273 | ) 274 | 275 | self.lld_file.write( 276 | '"{}" vulners.hosts_lld {}\n'.format( 277 | config.hosts_host, discovery_hosts_json 278 | ) 279 | ) 280 | 281 | def write_packages_data(self): 282 | logger.info("Creating a matrix of vulnerable packages of all hosts") 283 | 284 | pkg_matrix = {} 285 | 286 | for idx, host in enumerate(self.hosts, 1): 287 | packages = host.pop("packages", []) 288 | for package in packages: 289 | pkg = pkg_matrix.setdefault( 290 | package["name"], 291 | { 292 | "name": package["name"], 293 | "score": package["score"], 294 | "bulletin_id": package["bulletin_id"], 295 | "fix": package["fix"], 296 | "host_list": [], 297 | }, 298 | ) 299 | if host["name"] not in pkg["host_list"]: 300 | pkg["host_list"].append(host["name"]) 301 | logger.info( 302 | '[{} of {}] "{}". Successfully processed vulnerable packages: {}'.format( 303 | idx, self.total_hosts_cnt, host["name"], len(packages) 304 | ) 305 | ) 306 | pkg_matrix = list(pkg_matrix.values()) 307 | 308 | logger.info("Unique vulnerable packages processed: {}".format(len(pkg_matrix))) 309 | logger.info("Creating an LLD-data for package monitoring") 310 | 311 | discovery_pkg = [] 312 | 313 | for package in pkg_matrix: 314 | affected_hosts_cnt = len(package["host_list"]) 315 | name = package["name"] 316 | bulletin_id = package["bulletin_id"] 317 | score = package["score"] 318 | fix = package["fix"] 319 | 320 | self.data_file.write( 321 | '"{}" "vulners.packages[{}]" {}\n'.format( 322 | config.packages_host, name, affected_hosts_cnt 323 | ) 324 | ) 325 | 326 | discovery_pkg.append( 327 | { 328 | "{#PKG.ID}": name, 329 | "{#PKG.URL}": bulletin_id, 330 | "{#PKG.SCORE}": score, 331 | "{#PKG.FIX}": fix, 332 | "{#PKG.AFFECTED}": affected_hosts_cnt, 333 | "{#PKG.IMPACT}": int(affected_hosts_cnt * score), 334 | "{#PKG.HOSTS}": "\n".join(package["host_list"]), 335 | } 336 | ) 337 | 338 | discovery_pkg_json = json.dumps({"data": discovery_pkg}, separators=(",", ":")) 339 | 340 | self.lld_file.write( 341 | '"{}" vulners.packages_lld {}\n'.format( 342 | config.packages_host, discovery_pkg_json 343 | ) 344 | ) 345 | 346 | def write_bulletins_data(self): 347 | logger.info("Creating an bulletin-matrix") 348 | bulletin_matrix = {} 349 | for host in self.hosts: 350 | for bulletin in host["bulletins"]: 351 | bulletin_hosts = bulletin_matrix.setdefault( 352 | bulletin["name"], {"bulletin": bulletin, "host_list": []} 353 | ) 354 | if host["name"] not in bulletin_hosts["host_list"]: 355 | bulletin_hosts["host_list"].append(host["name"]) 356 | bulletin_matrix = list(bulletin_matrix.values()) 357 | 358 | logger.info( 359 | "Unique security bulletins processed: {}".format(len(bulletin_matrix)) 360 | ) 361 | logger.info("Creating an LLD-data for bulletin monitoring") 362 | 363 | discovery_data = [] 364 | 365 | for bulletin in bulletin_matrix: 366 | affected_hosts_cnt = len(bulletin["host_list"]) 367 | bulletin_name = bulletin["bulletin"]["name"] 368 | bulletin_score = bulletin["bulletin"]["score"] 369 | bulletin_impact = int(affected_hosts_cnt * bulletin_score) 370 | 371 | self.data_file.write( 372 | '"{}" vulners.bulletins[{}] {}\n'.format( 373 | config.bulletins_host, bulletin_name, affected_hosts_cnt 374 | ) 375 | ) 376 | 377 | discovery_data.append( 378 | { 379 | "{#BULLETIN.ID}": bulletin_name, 380 | "{#BULLETIN.SCORE}": bulletin_score, 381 | "{#BULLETIN.AFFECTED}": affected_hosts_cnt, 382 | "{#BULLETIN.IMPACT}": bulletin_impact, 383 | "{#BULLETIN.HOSTS}": "\n".join(bulletin["host_list"]), 384 | } 385 | ) 386 | 387 | discovery_json = json.dumps({"data": discovery_data}, separators=(",", ":")) 388 | self.lld_file.write( 389 | '"{}" vulners.bulletins_lld {}\n'.format( 390 | config.bulletins_host, discovery_json 391 | ) 392 | ) 393 | 394 | def write_cvss_and_aggregation_data(self): 395 | logger.info("Creating an CVSS Score-based host-lists") 396 | score_list = [] 397 | 398 | host_count_table = dict((score_value, 0) for score_value in range(0, 11)) 399 | for host in self.hosts: 400 | score_list.append(host["score"]) 401 | host_count_table[int(host["score"])] += 1 402 | 403 | if not score_list: 404 | score_list = [0] 405 | 406 | logger.info("Creating an aggregated data") 407 | 408 | agg_score_median = median(score_list) 409 | agg_score_mean = mean(score_list) 410 | agg_score_max = max(score_list) 411 | agg_score_min = min(score_list) 412 | 413 | for intScore in host_count_table: 414 | self.data_file.write( 415 | '"{}" vulners.hostsCountScore{} {}\n'.format( 416 | config.statistics_host, intScore, host_count_table.get(intScore) 417 | ) 418 | ) 419 | self.data_file.write( 420 | '"{}" vulners.TotalHosts {}\n'.format( 421 | config.statistics_host, self.total_hosts_cnt 422 | ) 423 | ) 424 | self.data_file.write( 425 | '"{}" vulners.scoreMedian {}\n'.format( 426 | config.statistics_host, agg_score_median 427 | ) 428 | ) 429 | self.data_file.write( 430 | '"{}" vulners.scoreAverage {}\n'.format( 431 | config.statistics_host, agg_score_mean 432 | ) 433 | ) 434 | self.data_file.write( 435 | '"{}" vulners.scoreMaximum {}\n'.format( 436 | config.statistics_host, agg_score_max 437 | ) 438 | ) 439 | self.data_file.write( 440 | '"{}" vulners.scoreMinimum {}\n'.format( 441 | config.statistics_host, agg_score_min 442 | ) 443 | ) 444 | 445 | def push_data(self): 446 | push_lld_cmd = "{} -z {} -p {} -i {}".format( 447 | config.zabbix_sender_bin, 448 | config.zbx_server_fqdn, 449 | config.zbx_server_port, 450 | self.lld_file_path, 451 | ) 452 | push_cmd = "{} -z {} -p {} -i {}".format( 453 | config.zabbix_sender_bin, 454 | config.zbx_server_fqdn, 455 | config.zbx_server_port, 456 | self.data_file_path, 457 | ) 458 | 459 | if args.nopush: 460 | logger.info( 461 | '"nopush" option is used. The transfer of data to zabbix is disabled, but can be performed by commands:' 462 | ) 463 | logger.info("{}; sleep 300; {}".format(push_lld_cmd, push_cmd)) 464 | else: 465 | logger.info("Pushing LLD-objects to Zabbix: {}".format(push_lld_cmd)) 466 | logger.info(self.shell(push_lld_cmd)) 467 | logger.info("Awaiting for 5 min") 468 | sleep(300) 469 | 470 | logger.info("Pushing data to Zabbix: {}".format(push_cmd)) 471 | logger.info(self.shell(push_cmd)) 472 | self.data_file.close() 473 | self.lld_file.close() 474 | logger.info("Work completed successfully") 475 | 476 | def open_files(self): 477 | self.data_file = open(self.data_file_path, "w") 478 | self.lld_file = open(self.lld_file_path, "w") 479 | 480 | def close_files(self): 481 | self.data_file.close() 482 | self.lld_file.close() 483 | 484 | def run(self): 485 | self.get_or_create_hosts_matrix() 486 | 487 | if args.dump: 488 | self.create_hosts_dump() 489 | logger.info("hosts-matrix saved to {}".format(self.hosts_dump_path)) 490 | 491 | self.total_hosts_cnt = len(self.hosts) 492 | self.open_files() 493 | self.write_score_data() 494 | self.write_packages_data() 495 | self.write_bulletins_data() 496 | self.write_cvss_and_aggregation_data() 497 | self.close_files() 498 | self.push_data() 499 | 500 | 501 | if __name__ == "__main__": 502 | parser = argparse.ArgumentParser(description="Vulners to zabbix integration tool") 503 | parser.add_argument( 504 | "-n", 505 | "--nopush", 506 | help="Bypass Zabbix-server. Don't push final dataset to Zabbix-server.", 507 | action="store_true", 508 | ) 509 | 510 | parser.add_argument( 511 | "-d", "--dump", help="Dump zabbix and vulners data to disk", action="store_true" 512 | ) 513 | 514 | parser.add_argument( 515 | "-l", 516 | "--limit", 517 | type=int, 518 | help="Host limit for processing. Only the specified number of hosts will be received from the Zabbix.", 519 | ) 520 | 521 | args = parser.parse_args() 522 | 523 | logger = logging.getLogger("ZTC") 524 | if config.debug_level == 0: 525 | logger.setLevel(logging.ERROR) 526 | elif config.debug_level == 2: 527 | logger.setLevel(logging.DEBUG) 528 | else: 529 | logger.setLevel(logging.INFO) 530 | 531 | fh = logging.FileHandler(config.log_file) 532 | 533 | formatter = logging.Formatter( 534 | "%(asctime)s %(name)s %(levelname)s %(message)s [%(filename)s:%(lineno)d]" 535 | ) 536 | fh.setFormatter(formatter) 537 | logger.addHandler(fh) 538 | try: 539 | Scan().run() 540 | except Exception as e: 541 | logger.exception(e) 542 | raise 543 | -------------------------------------------------------------------------------- /ztc.conf: -------------------------------------------------------------------------------- 1 | [MANDATORY] 2 | ### Option: VulnersApiKey 3 | # Used to access the API. 4 | # To get it follow the steps bellow: 5 | # Log in to vulners.com. Navigate to userinfo space https://vulners.com/userinfo. 6 | # Choose "API KEYS" section. Select "scan" in scope menu and click "Generate new key". 7 | # Example api key = RGB9YPJG7CFAXP35PMDVYFFJPGZ9ZIRO1VGO9K9269B0K86K6XQQQR32O6007NUK 8 | # 9 | # Mandatory: yes 10 | # Default: 11 | VulnersApiKey = 12 | 13 | 14 | ### Option: ZabbixApiUser 15 | # Username for access to Zabbix API 16 | # Account should have rights to create the objects in Zabbix: groups, hosts, dashboards, templates, actions. 17 | # 18 | # Mandatory: yes 19 | # Default: 20 | ZabbixApiUser = 21 | 22 | 23 | ### Option: ZabbixApiPassword 24 | # User password to access the Zabbix API 25 | # 26 | # Mandatory: yes 27 | # Default: 28 | ZabbixApiPassword = 29 | 30 | 31 | [OPTIONAL] 32 | ### Option: VulnersProxyHost 33 | # The URL to Vulners Proxy host 34 | # 35 | # Mandatory: no 36 | # Default: 37 | # VulnersProxyHost = https://vulners.com 38 | 39 | 40 | ### Option: ZabbixFrontUrl 41 | # The URL to the zabbix frontend, to access the Zabbix API 42 | # 43 | # Mandatory: no 44 | # Default: 45 | # ZabbixFrontUrl = http://localhost 46 | 47 | 48 | ### Option: VerifySSL 49 | # Check SSL certificate for zabbix frontend for validity. 50 | # If you use a self-signed certificate or if you have problems verifying the certificate - use False 51 | # True/False 52 | # 53 | # Mandatory: no 54 | # Default: 55 | # VerifySSL = True 56 | 57 | 58 | ### Option: ZabbixServerFQDN 59 | # The domain name (FQDN) of the zabbix-server, not the IP address. 60 | # 61 | # Mandatory: no 62 | # Default: 63 | # ZabbixServerFQDN = localhost 64 | 65 | 66 | ### Option: ZabbixServerPort 67 | # The TCP-port of the zabbix-server. 68 | # 69 | # Mandatory: no 70 | # Default: 71 | # ZabbixServerPort = 10051 72 | 73 | 74 | ### Option: TrustedZabbixUsers 75 | # Zabbix users who can initiate the execution of fix-commands. 76 | # A list of usernames separated by commas. 77 | # 78 | # Mandatory: no 79 | # Default: 80 | # TrustedZabbixUsers = Admin 81 | 82 | 83 | ### Option: UseZabbixAgentToFix 84 | # Execute fix commands on the target servers using the Zabbix agent. 85 | # True/False 86 | # 87 | # Mandatory: no 88 | # Default: 89 | # UseZabbixAgentToFix = True 90 | 91 | 92 | ### Option: SSHUser 93 | # A user who can connect to the target servers by ssh to execute fix commands 94 | # 95 | # Mandatory: no 96 | # Default: 97 | # SSHUser = True 98 | 99 | 100 | ### Option: WorkDir 101 | # 102 | # 103 | # Mandatory: no 104 | # Default: 105 | # WorkDir = /opt/monitoring/zabbix-threat-control 106 | 107 | 108 | ### Option: LogFile 109 | # 110 | # 111 | # Mandatory: no 112 | # Default: 113 | # LogFile = /var/log/zabbix-threat-control.log 114 | 115 | 116 | ### Option: DebugLevel 117 | # Specifies debug level: 118 | # 0 - only error information 119 | # 1 - basic information 120 | # 2 - for debugging 121 | # 122 | # Mandatory: no 123 | # Default: 124 | # DebugLevel = 1 125 | 126 | 127 | ### Option: HostsApplicationName 128 | # 129 | # 130 | # Mandatory: no 131 | # Default: 132 | # HostsApplicationName = Vulnerabilities 133 | 134 | 135 | ### Option: DashboardName 136 | # 137 | # 138 | # Mandatory: no 139 | # Default: 140 | # DashboardName = Vulners 141 | 142 | 143 | ### Option: ActionName 144 | # 145 | # 146 | # Mandatory: no 147 | # Default: 148 | # ActionName = Vulners 149 | 150 | 151 | ### Option: HostsHost 152 | # 153 | # 154 | # Mandatory: no 155 | # Default: 156 | # HostsHost = vulners.hosts 157 | 158 | 159 | ### Option: HostsVisibleName 160 | # 161 | # 162 | # Mandatory: no 163 | # Default: 164 | # HostsVisibleName = Vulners - Hosts 165 | 166 | 167 | ### Option: BulletinsHost 168 | # 169 | # 170 | # Mandatory: no 171 | # Default: 172 | # BulletinsHost = vulners.bulletins 173 | 174 | 175 | ### Option: BulletinsVisibleName 176 | # 177 | # 178 | # Mandatory: no 179 | # Default: 180 | # BulletinsVisibleName = Vulners - Bulletins 181 | 182 | 183 | ### Option: PackagesHost 184 | # 185 | # 186 | # Mandatory: no 187 | # Default: 188 | # PackagesHost = vulners.packages 189 | 190 | 191 | ### Option: PackagesVisibleName 192 | # 193 | # 194 | # Mandatory: no 195 | # Default: 196 | # PackagesVisibleName = Vulners - Packages 197 | 198 | 199 | ### Option: StatisticsHost 200 | # 201 | # 202 | # Mandatory: no 203 | # Default: 204 | # StatisticsHost = vulners.statistics 205 | 206 | 207 | ### Option: StatisticsVisibleName 208 | # 209 | # 210 | # Mandatory: no 211 | # Default: 212 | # StatisticsVisibleName = Vulners - Statistics 213 | 214 | 215 | ### Option: StatisticsMacrosName 216 | # 217 | # 218 | # Mandatory: no 219 | # Default: 220 | # StatisticsMacrosName = {$WORK_SCRIPT_CMD} 221 | 222 | 223 | ### Option: StatisticsMacrosValue 224 | # 225 | # 226 | # Mandatory: no 227 | # Default: 228 | # StatisticsMacrosValue = /opt/monitoring/zabbix-threat-control/scan.py 229 | 230 | 231 | ### Option: TemplateHost 232 | # 233 | # 234 | # Mandatory: no 235 | # Default: 236 | # TemplateHost = tmpl.vulners.os-report 237 | 238 | 239 | ### Option: TemplateVisibleName 240 | # 241 | # 242 | # Mandatory: no 243 | # Default: 244 | # TemplateVisibleName = Template Vulners OS-Report 245 | 246 | 247 | ### Option: TemplateMacrosName 248 | # 249 | # 250 | # Mandatory: no 251 | # Default: 252 | # TemplateMacrosName = {$REPORT_SCRIPT_PATH} 253 | 254 | 255 | ### Option: TemplateMacrosValue 256 | # 257 | # 258 | # Mandatory: no 259 | # Default: 260 | # TemplateMacrosValue = /opt/monitoring/os-report/report.py 261 | 262 | 263 | ### Option: TemplateApplicationName 264 | # 265 | # 266 | # Mandatory: no 267 | # Default: 268 | # TemplateApplicationName = Vulners OS Report 269 | 270 | 271 | ### Option: HostGroupName 272 | # 273 | # 274 | # Mandatory: no 275 | # Default: 276 | # HostGroupName = Vulners 277 | 278 | 279 | ### Option: TemplateGroupName 280 | # 281 | # 282 | # Mandatory: no 283 | # Default: 284 | # TemplateGroupName = Templates 285 | 286 | 287 | ### Option: ZabbixGet 288 | # 289 | # 290 | # Mandatory: no 291 | # Default: 292 | # ZabbixGet = zabbix_get 293 | 294 | 295 | ### Option: ZabbixSender 296 | # 297 | # 298 | # Mandatory: no 299 | # Default: 300 | # ZabbixSender = zabbix_sender 301 | 302 | 303 | ### Option: MinCVSS 304 | # 305 | # 306 | # Mandatory: no 307 | # Default: 308 | # MinCVSS = 1 309 | --------------------------------------------------------------------------------