├── .devops └── go1.20编译二进制文件.yml ├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── agent ├── agent.go ├── funcs.go └── runner.go ├── asset └── changelog.md ├── build.sh ├── conf.d ├── config.toml ├── p.exec │ └── exec.toml ├── p.filechange │ └── filechange.toml ├── p.http │ └── http.toml ├── p.journaltail │ └── journaltail.toml ├── p.mtime │ └── mtime.toml ├── p.net │ └── net.toml ├── p.ping │ └── ping.toml ├── p.procnum │ └── procnum.toml └── p.sfilter │ └── sfilter.toml ├── config ├── config.go ├── duration.go ├── http_config.go ├── inline.go └── version.go ├── docker └── Dockerfile.goreleaser ├── engine ├── cache.go └── engine.go ├── go.mod ├── go.sum ├── logger └── logger.go ├── main.go ├── pkg ├── cfg │ ├── cfg.go │ └── scan.go ├── choice │ └── choice.go ├── cmdx │ ├── cmd_notwindows.go │ ├── cmd_windows.go │ └── cmdx.go ├── filter │ └── filter.go ├── netx │ └── netx.go ├── osx │ ├── osx.go │ └── proc.go ├── runtimex │ └── stack.go ├── safe │ └── queue.go ├── shell │ └── shellquote.go └── tls │ ├── common.go │ └── config.go ├── plugins ├── exec │ └── exec.go ├── filechange │ └── filechange.go ├── http │ ├── http.go │ └── tls.go ├── journaltail │ └── journaltail.go ├── mtime │ └── mtime.go ├── net │ └── net.go ├── ping │ └── ping.go ├── plugins.go ├── procnum │ ├── native_finder.go │ ├── native_finder_notwindows.go │ ├── native_finder_windows.go │ ├── process.go │ ├── procnum.go │ ├── win_service_notwindows.go │ └── win_service_windows.go └── sfilter │ └── sfilter.go ├── scripts ├── demo.sh ├── df.sh ├── greplog.sh └── ulimit.sh ├── types └── event.go └── winx ├── winx_posix.go └── winx_windows.go /.devops/go1.20编译二进制文件.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | name: go1.20编译二进制文件 3 | description: 显示二进制文件信息,可使用scp节点复制到其他服务器 4 | global: 5 | concurrent: 1 6 | trigger: 7 | webhook: gitlink@1.0.0 8 | event: 9 | - ref: create_tag 10 | ruleset: 11 | - param-ref: tag 12 | operator: REG_EXP 13 | value: '"v.*"' 14 | ruleset-operator: AND 15 | workflow: 16 | - ref: git_clone_0 17 | name: git clone 18 | task: git_clone@1.2.9 19 | input: 20 | remote_url: '"https://gitlink.org.cn/UlricQin/catpaw.git"' 21 | ref: '"refs/heads/master"' 22 | commit_id: '""' 23 | depth: 1 24 | needs: 25 | - start 26 | - ref: start 27 | name: 开始 28 | task: start 29 | - ref: end 30 | name: 结束 31 | task: end 32 | needs: 33 | - shell_0 34 | - ref: golang_build_node_0 35 | name: golang_build_node 36 | task: yystopf/golang_build_node@0.0.2 37 | input: 38 | workspace: git_clone_0.git_path 39 | out_bin_name: '"build-test-bin"' 40 | goos: '"linux"' 41 | goarch: '"amd64"' 42 | needs: 43 | - git_clone_0 44 | - ref: shell_0 45 | name: 显示go build bin文件 46 | image: docker.jianmuhub.com/library/alpine:3.17.0 47 | env: 48 | GO_BIN_FILE: golang_build_node_0.bin_dir 49 | script: 50 | - ls -lsh ${GO_BIN_FILE} 51 | needs: 52 | - golang_build_node_0 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | permissions: write-all 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | env: 9 | GO_VERSION: 1.22 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - name: Checkout Source Code 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Go Environment 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: ${{ env.GO_VERSION }} 23 | - uses: docker/login-action@v2 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@v3 29 | with: 30 | version: '~> v1' 31 | args: release --rm-dist 32 | distribution: goreleaser 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /catpaw* 2 | /nohup.out 3 | /vendor 4 | /dist 5 | 6 | .idea 7 | .DS_Store 8 | .vscode 9 | .log -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | # You may remove this if you don't use go modules. 4 | - go mod tidy 5 | 6 | snapshot: 7 | name_template: '{{ .Tag }}' 8 | checksum: 9 | name_template: 'checksums.txt' 10 | changelog: 11 | skip: true 12 | 13 | builds: 14 | - id: build 15 | main: ./ 16 | binary: catpaw 17 | env: 18 | - CGO_ENABLED=0 19 | goos: 20 | - linux 21 | - windows 22 | goarch: 23 | - arm64 24 | - amd64 25 | ldflags: 26 | - -s -w 27 | - -X github.com/cprobe/catpaw/config.Version={{ .Tag }}-{{.Commit}} 28 | 29 | archives: 30 | - id: archive 31 | rlcp: true 32 | builds: 33 | - build 34 | format: tar.gz 35 | format_overrides: 36 | - goos: windows 37 | format: zip 38 | name_template: "{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}" 39 | wrap_in_directory: true 40 | files: 41 | - conf.d/* 42 | - LICENSE 43 | - README.md 44 | 45 | 46 | release: 47 | github: 48 | owner: cprobe 49 | name: catpaw 50 | name_template: "v{{ .Version }}" 51 | 52 | dockers: 53 | - image_templates: 54 | - flashcatcloud/catpaw:{{ .Tag }}-amd64 55 | goos: linux 56 | goarch: amd64 57 | ids: 58 | - build 59 | dockerfile: docker/Dockerfile.goreleaser 60 | extra_files: 61 | - LICENSE 62 | use: buildx 63 | build_flag_templates: 64 | - "--platform=linux/amd64" 65 | 66 | - image_templates: 67 | - flashcatcloud/catpaw:{{ .Tag }}-arm64v8 68 | goos: linux 69 | goarch: arm64 70 | ids: 71 | - build 72 | dockerfile: docker/Dockerfile.goreleaser 73 | extra_files: 74 | - LICENSE 75 | use: buildx 76 | build_flag_templates: 77 | - "--platform=linux/arm64/v8" 78 | 79 | docker_manifests: 80 | - name_template: flashcatcloud/catpaw:{{ .Tag }} 81 | image_templates: 82 | - flashcatcloud/catpaw:{{ .Tag }}-amd64 83 | - flashcatcloud/catpaw:{{ .Tag }}-arm64v8 84 | 85 | - name_template: flashcatcloud/catpaw:latest 86 | image_templates: 87 | - flashcatcloud/catpaw:{{ .Tag }}-amd64 88 | - flashcatcloud/catpaw:{{ .Tag }}-arm64v8 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 这是一个极为轻量的事件监控系统,用于探测一些异常事件并生成告警。通常和 Flashduty 协同使用(当然,你也可以写个接收事件的小服务来做告警分发),catpaw 负责产生事件,Flashduty 负责发送事件。 3 | 4 | ## 插件简介 5 | catpaw 是插件机制,提供了不同功能的插件用于不同的监控场景。 6 | 7 | ### exec 8 | 自定义脚本执行的插件。脚本用什么语言写都可以,只要按照规定的格式输出即可。 9 | 10 | ### filechange 11 | 监控近期是否有文件发生变化,比如 `/etc/shadow` 等重要文件。 12 | 13 | ### http 14 | 监控 HTTP URL,检查返回的状态码和内容是否符合预期。 15 | 16 | ### journaltail 17 | 使用 journalctl 命令检查日志,如果日志里有关键字就产生事件。 18 | 19 | ### mtime 20 | 递归检查某个目录下的所有文件的 mtime,如果有文件在近期发生变化就产生事件。 21 | 22 | ### net 23 | 通过 tcp、udp 方式探测远端端口是否可用。 24 | 25 | ### ping 26 | 通过 icmp 方式探测远端主机是否可用。 27 | 28 | ### procnum 29 | 检查某个进程的数量,如果数量不够(通常是进程挂了)就产生事件。 30 | 31 | ### sfilter 32 | 执行脚本,检查输出,只要输出中包含关键字就产生事件。 33 | 34 | ## 使用场景 35 | 36 | - 不想引入大型监控系统,不想有太多依赖,就想对一些重要的事情做一些简单的监控。 37 | - 监控系统的自监控。为了避免循环依赖,对监控系统做监控,通常需要另一个系统,catpaw 轻量,合适。 38 | - 对一些日志、字符串、事件文本做监控,直接读取匹配了关键字就告警。 39 | 40 | ## 安装 41 | 42 | 从 [github releases](https://github.com/cprobe/catpaw/releases) 页面下载编译好的二进制。 43 | 44 | ## 使用 45 | 46 | 首先你需要注册一个 Flashduty 账号。 47 | 48 | - [Flashduty产品介绍](https://flashcat.cloud/product/flashduty/) 49 | - [Flashduty免费注册](https://console.flashcat.cloud/) 50 | 51 | 然后在集成中心创建一个“标准告警事件”的集成,随便起个名字,保存,就可以得到一个 webhook 地址。如果搞不定,Flashduty 页面右上角有较为详细的文档和视频教程。 52 | 53 | 把 webhook 地址配置到 catpaw 的配置文件中:`conf.d/config.toml`,配置到 flashduty 下面的 url 字段。然后,就可以启动 catpaw 玩耍了。catpaw 有几个命令行参数,通过 `./catpaw --help` 可以看到。 54 | 55 | 当然了,具体要监控什么,需要去修改各个插件的配置,每个插件的配置文件在 `conf.d` 目录下,比如 `conf.d/p.http` 就是 http 插件的配置文件。里边有详尽的注释。 56 | 57 | ## 交流 58 | 59 | 可以加我微信:`picobyte` 进群交流。备注 `catpaw`。 60 | 61 | -------------------------------------------------------------------------------- /agent/agent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "flashcat.cloud/catpaw/config" 9 | "flashcat.cloud/catpaw/logger" 10 | "flashcat.cloud/catpaw/pkg/choice" 11 | "flashcat.cloud/catpaw/plugins" 12 | "github.com/BurntSushi/toml" 13 | "github.com/toolkits/pkg/file" 14 | 15 | // auto registry 16 | _ "flashcat.cloud/catpaw/plugins/exec" 17 | _ "flashcat.cloud/catpaw/plugins/filechange" 18 | _ "flashcat.cloud/catpaw/plugins/http" 19 | _ "flashcat.cloud/catpaw/plugins/journaltail" 20 | _ "flashcat.cloud/catpaw/plugins/mtime" 21 | _ "flashcat.cloud/catpaw/plugins/net" 22 | _ "flashcat.cloud/catpaw/plugins/ping" 23 | _ "flashcat.cloud/catpaw/plugins/procnum" 24 | _ "flashcat.cloud/catpaw/plugins/sfilter" 25 | ) 26 | 27 | type PluginConfig struct { 28 | Source string // file || http 29 | Digest string 30 | FileContent []byte 31 | } 32 | 33 | type Agent struct { 34 | pluginFilters map[string]struct{} 35 | pluginConfigs map[string]*PluginConfig 36 | pluginRunners map[string]*PluginRunner 37 | sync.RWMutex 38 | } 39 | 40 | func New() *Agent { 41 | return &Agent{ 42 | pluginFilters: parseFilter(config.Config.Plugins), 43 | pluginConfigs: make(map[string]*PluginConfig), 44 | pluginRunners: make(map[string]*PluginRunner), 45 | } 46 | } 47 | 48 | func (a *Agent) Start() { 49 | logger.Logger.Info("agent starting") 50 | 51 | pcs, err := loadFileConfigs() 52 | if err != nil { 53 | logger.Logger.Error("load file configs fail:", err) 54 | return 55 | } 56 | 57 | for name, pc := range pcs { 58 | a.LoadPlugin(name, pc) 59 | } 60 | 61 | logger.Logger.Info("agent started") 62 | } 63 | 64 | func (a *Agent) LoadPlugin(name string, pc *PluginConfig) { 65 | if len(a.pluginFilters) > 0 { 66 | // need filter by --plugins 67 | _, has := a.pluginFilters[name] 68 | if !has { 69 | return 70 | } 71 | } 72 | 73 | logger.Logger.Infof("%s: loading...", name) 74 | 75 | creator, has := plugins.PluginCreators[name] 76 | if !has { 77 | logger.Logger.Infof("%s: plugin not supported", name) 78 | return 79 | } 80 | 81 | pluginObject := creator() 82 | err := toml.Unmarshal(pc.FileContent, pluginObject) 83 | if err != nil { 84 | logger.Logger.Errorf("%s: unmarshal plugin config fail: %v", name, err) 85 | return 86 | } 87 | 88 | // structs will have value after toml.Unmarshal 89 | // apply partial configuration if some fields are not set 90 | err = plugins.MayApplyPartials(pluginObject) 91 | if err != nil { 92 | logger.Logger.Errorf("%s: apply partial config fail: %v", name, err) 93 | return 94 | } 95 | 96 | runner := newPluginRunner(name, pluginObject) 97 | runner.start() 98 | 99 | a.Lock() 100 | a.pluginRunners[name] = runner 101 | a.pluginConfigs[name] = pc 102 | a.Unlock() 103 | } 104 | 105 | func (a *Agent) DelPlugin(name string) { 106 | a.Lock() 107 | defer a.Unlock() 108 | 109 | if runner, has := a.pluginRunners[name]; has { 110 | runner.stop() 111 | delete(a.pluginRunners, name) 112 | delete(a.pluginConfigs, name) 113 | } 114 | } 115 | 116 | func (a *Agent) RunningPlugins() []string { 117 | a.RLock() 118 | defer a.RUnlock() 119 | 120 | cnt := len(a.pluginRunners) 121 | ret := make([]string, 0, cnt) 122 | 123 | for name := range a.pluginRunners { 124 | ret = append(ret, name) 125 | } 126 | 127 | return ret 128 | } 129 | 130 | func (a *Agent) GetPluginConfig(name string) *PluginConfig { 131 | a.RLock() 132 | defer a.RUnlock() 133 | 134 | return a.pluginConfigs[name] 135 | } 136 | 137 | func (a *Agent) Stop() { 138 | logger.Logger.Info("agent stopping") 139 | 140 | a.Lock() 141 | defer a.Unlock() 142 | 143 | for name := range a.pluginRunners { 144 | a.pluginRunners[name].stop() 145 | delete(a.pluginRunners, name) 146 | delete(a.pluginConfigs, name) 147 | } 148 | 149 | logger.Logger.Info("agent stopped") 150 | } 151 | 152 | func (a *Agent) HandleChangedPlugin(names []string) { 153 | for _, name := range names { 154 | pc := a.GetPluginConfig(name) 155 | if pc.Source != "file" { 156 | // not supported 157 | continue 158 | } 159 | 160 | mtime, err := getMTime(name) 161 | if err != nil { 162 | logger.Logger.Errorw("get mtime fail:"+err.Error(), "plugin:", name) 163 | continue 164 | } 165 | 166 | if mtime == -1 { 167 | // files deleted 168 | a.DelPlugin(name) 169 | continue 170 | } 171 | 172 | if pc.Digest == fmt.Sprint(mtime) { 173 | // not changed 174 | continue 175 | } 176 | 177 | // configuration changed 178 | // delete old plugin 179 | a.DelPlugin(name) 180 | 181 | bs, err := getFileContent(name) 182 | if err != nil { 183 | logger.Logger.Errorw("get file content fail:"+err.Error(), "plugin:", name) 184 | continue 185 | } 186 | 187 | if bs == nil { 188 | // files deleted 189 | continue 190 | } 191 | 192 | a.LoadPlugin(name, &PluginConfig{ 193 | Source: "file", 194 | Digest: fmt.Sprint(mtime), 195 | FileContent: bs, 196 | }) 197 | } 198 | } 199 | 200 | func (a *Agent) Reload() { 201 | logger.Logger.Info("agent reloading") 202 | 203 | names := a.RunningPlugins() 204 | a.HandleChangedPlugin(names) 205 | a.HandleNewPlugin(names) 206 | 207 | logger.Logger.Info("agent reloaded") 208 | } 209 | 210 | func (a *Agent) HandleNewPlugin(names []string) { 211 | dirs, err := file.DirsUnder(config.Config.ConfigDir) 212 | if err != nil { 213 | logger.Logger.Error("failed to get config dirs:", err) 214 | return 215 | } 216 | 217 | for _, dir := range dirs { 218 | if !strings.HasPrefix(dir, "p.") { 219 | continue 220 | } 221 | 222 | name := dir[len("p."):] 223 | 224 | if choice.Contains(name, names) { 225 | // already running 226 | continue 227 | } 228 | 229 | mtime, err := getMTime(name) 230 | if err != nil { 231 | logger.Logger.Error("get mtime fail:", err) 232 | continue 233 | } 234 | 235 | if mtime == -1 { 236 | continue 237 | } 238 | 239 | bs, err := getFileContent(name) 240 | if err != nil { 241 | logger.Logger.Error("get file content fail:", err) 242 | continue 243 | } 244 | 245 | if bs == nil { 246 | continue 247 | } 248 | 249 | a.LoadPlugin(name, &PluginConfig{ 250 | Source: "file", 251 | Digest: fmt.Sprint(mtime), 252 | FileContent: bs, 253 | }) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /agent/funcs.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "sort" 7 | "strings" 8 | 9 | "flashcat.cloud/catpaw/config" 10 | "github.com/toolkits/pkg/file" 11 | ) 12 | 13 | func loadFileConfigs() (map[string]*PluginConfig, error) { 14 | dirs, err := file.DirsUnder(config.Config.ConfigDir) 15 | if err != nil { 16 | return nil, fmt.Errorf("failed to get config dirs: %v", err) 17 | } 18 | 19 | ret := make(map[string]*PluginConfig) 20 | 21 | for _, dir := range dirs { 22 | if !strings.HasPrefix(dir, "p.") { 23 | continue 24 | } 25 | 26 | // use this as map key 27 | name := dir[len("p."):] 28 | 29 | pluginDir := path.Join(config.Config.ConfigDir, dir) 30 | files, err := file.FilesUnder(pluginDir) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to list files under %s: %v", pluginDir, err) 33 | } 34 | 35 | if len(files) == 0 { 36 | continue 37 | } 38 | 39 | sort.Strings(files) 40 | 41 | var maxmt int64 42 | var bytes []byte 43 | for i := 0; i < len(files); i++ { 44 | if !strings.HasSuffix(files[i], ".toml") { 45 | continue 46 | } 47 | 48 | filepath := path.Join(pluginDir, files[i]) 49 | mtime, err := file.FileMTime(filepath) 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to get mtime of %s: %v", filepath, err) 52 | } 53 | 54 | if mtime > maxmt { 55 | maxmt = mtime 56 | } 57 | 58 | if i > 0 { 59 | bytes = append(bytes, '\n') 60 | bytes = append(bytes, '\n') 61 | } 62 | 63 | bs, err := file.ReadBytes(filepath) 64 | if err != nil { 65 | return nil, fmt.Errorf("failed to read %s: %v", filepath, err) 66 | } 67 | 68 | bytes = append(bytes, bs...) 69 | } 70 | 71 | ret[name] = &PluginConfig{ 72 | Digest: fmt.Sprint(maxmt), 73 | FileContent: bytes, 74 | Source: "file", 75 | } 76 | } 77 | 78 | return ret, nil 79 | } 80 | 81 | // return -1 means no configuration files under plugin directory 82 | func getMTime(name string) (int64, error) { 83 | pluginDir := path.Join(config.Config.ConfigDir, "p."+name) 84 | 85 | files, err := file.FilesUnder(pluginDir) 86 | if err != nil { 87 | return 0, fmt.Errorf("failed to list files under %s: %v", pluginDir, err) 88 | } 89 | 90 | var maxmt int64 = -1 91 | for i := 0; i < len(files); i++ { 92 | if !strings.HasSuffix(files[i], ".toml") { 93 | continue 94 | } 95 | 96 | filepath := path.Join(pluginDir, files[i]) 97 | mtime, err := file.FileMTime(filepath) 98 | if err != nil { 99 | return 0, fmt.Errorf("failed to get mtime of %s: %v", filepath, err) 100 | } 101 | 102 | if mtime > maxmt { 103 | maxmt = mtime 104 | } 105 | } 106 | 107 | return maxmt, nil 108 | } 109 | 110 | // get plugin configuration file content 111 | func getFileContent(name string) ([]byte, error) { 112 | pluginDir := path.Join(config.Config.ConfigDir, "p."+name) 113 | 114 | files, err := file.FilesUnder(pluginDir) 115 | if err != nil { 116 | return nil, fmt.Errorf("failed to list files under %s: %v", pluginDir, err) 117 | } 118 | 119 | if len(files) == 0 { 120 | return nil, nil 121 | } 122 | 123 | sort.Strings(files) 124 | 125 | var bytes []byte 126 | for i := 0; i < len(files); i++ { 127 | if !strings.HasSuffix(files[i], ".toml") { 128 | continue 129 | } 130 | 131 | filepath := path.Join(pluginDir, files[i]) 132 | 133 | if i > 0 { 134 | bytes = append(bytes, '\n') 135 | bytes = append(bytes, '\n') 136 | } 137 | 138 | bs, err := file.ReadBytes(filepath) 139 | if err != nil { 140 | return nil, fmt.Errorf("failed to read %s: %v", filepath, err) 141 | } 142 | 143 | bytes = append(bytes, bs...) 144 | } 145 | 146 | return bytes, nil 147 | } 148 | 149 | func parseFilter(filterStr string) map[string]struct{} { 150 | filters := strings.Split(filterStr, ":") 151 | filtermap := make(map[string]struct{}) 152 | for i := 0; i < len(filters); i++ { 153 | if strings.TrimSpace(filters[i]) == "" { 154 | continue 155 | } 156 | filtermap[filters[i]] = struct{}{} 157 | } 158 | return filtermap 159 | } 160 | -------------------------------------------------------------------------------- /agent/runner.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "time" 5 | 6 | "flashcat.cloud/catpaw/config" 7 | "flashcat.cloud/catpaw/engine" 8 | "flashcat.cloud/catpaw/logger" 9 | "flashcat.cloud/catpaw/pkg/runtimex" 10 | "flashcat.cloud/catpaw/pkg/safe" 11 | "flashcat.cloud/catpaw/plugins" 12 | "flashcat.cloud/catpaw/types" 13 | ) 14 | 15 | type PluginRunner struct { 16 | pluginName string 17 | pluginObject plugins.Plugin 18 | quitChan []chan struct{} 19 | Instances []plugins.Instance 20 | } 21 | 22 | func newPluginRunner(pluginName string, p plugins.Plugin) *PluginRunner { 23 | return &PluginRunner{ 24 | pluginName: pluginName, 25 | pluginObject: p, 26 | } 27 | } 28 | 29 | func (r *PluginRunner) stop() { 30 | for i := 0; i < len(r.Instances); i++ { 31 | r.quitChan[i] <- struct{}{} 32 | plugins.MayDrop(r.Instances[i]) 33 | } 34 | } 35 | 36 | func (r *PluginRunner) start() { 37 | r.Instances = plugins.MayGetInstances(r.pluginObject) 38 | r.quitChan = make([]chan struct{}, len(r.Instances)) 39 | for i := 0; i < len(r.Instances); i++ { 40 | r.quitChan[i] = make(chan struct{}, 1) 41 | ins := r.Instances[i] 42 | ch := r.quitChan[i] 43 | go r.startInstancePlugin(ins, ch) 44 | time.Sleep(50 * time.Millisecond) 45 | } 46 | } 47 | 48 | func (r *PluginRunner) startInstancePlugin(instance plugins.Instance, ch chan struct{}) { 49 | interval := instance.GetInterval() 50 | if interval == 0 { 51 | interval = r.pluginObject.GetInterval() 52 | if interval == 0 { 53 | interval = config.Config.Global.Interval 54 | } 55 | } 56 | 57 | if err := instance.InitInternalConfig(); err != nil { 58 | logger.Logger.Errorw("init internal config fail: "+err.Error(), "plugin", r.pluginName) 59 | return 60 | } 61 | 62 | timer := time.NewTimer(0) 63 | defer timer.Stop() 64 | 65 | var start time.Time 66 | 67 | for { 68 | select { 69 | case <-ch: 70 | close(ch) 71 | return 72 | case <-timer.C: 73 | start = time.Now() 74 | r.gatherInstancePlugin(instance) 75 | next := time.Duration(interval) - time.Since(start) 76 | if next < 0 { 77 | next = 0 78 | } 79 | timer.Reset(next) 80 | } 81 | } 82 | } 83 | 84 | func (r *PluginRunner) gatherInstancePlugin(ins plugins.Instance) { 85 | defer func() { 86 | if rc := recover(); rc != nil { 87 | logger.Logger.Errorw("gather instance plugin panic: "+string(runtimex.Stack(3)), "plugin", r.pluginName) 88 | } 89 | }() 90 | 91 | queue := safe.NewQueue[*types.Event]() 92 | plugins.MayGather(ins, queue) 93 | if queue.Len() > 0 { 94 | engine.PushRawEvents(r.pluginName, r.pluginObject, ins, queue) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /asset/changelog.md: -------------------------------------------------------------------------------- 1 | ## v0.1.2 2 | 3 | - New: add journaltail plugin 4 | - New: add script greplog.sh 5 | 6 | ## v0.3.0 7 | 8 | - New: add mtime plugin 9 | 10 | ## v0.4.0 11 | 12 | - New: add sfilter plugin 13 | - Fix: remove configuration keywords of plugin journaltail 14 | 15 | ## v0.6.0 16 | 17 | - New: add filechange plugin 18 | 19 | ## v0.7.0 20 | 21 | - New: add procnum plugin 22 | 23 | ## v0.8.0 24 | 25 | - New: refactor http/net/ping plugin configurations 26 | 27 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export GOPROXY=https://goproxy.cn 4 | 5 | if ! go build; then 6 | echo "build failed" 7 | exit 1 8 | fi 9 | 10 | version=$(./catpaw -version) 11 | echo "version: $version" 12 | 13 | rm -rf dist/catpaw-v${version}-linux-amd64 14 | mkdir -p dist/catpaw-v${version}-linux-amd64 15 | 16 | cp catpaw dist/catpaw-v${version}-linux-amd64/ 17 | cp -r conf.d dist/catpaw-v${version}-linux-amd64/ 18 | cp -r scripts dist/catpaw-v${version}-linux-amd64/ 19 | 20 | cd dist 21 | tar -zcvf catpaw-v${version}-linux-amd64.tar.gz catpaw-v${version}-linux-amd64 22 | 23 | echo "build success" -------------------------------------------------------------------------------- /conf.d/config.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | interval = "30s" 3 | 4 | [global.labels] 5 | from_agent = "catpaw" 6 | from_hostname = "$hostname" 7 | from_hostip = "$ip" 8 | 9 | [log] 10 | level = "info" 11 | # format = "json" 12 | # output = "stdout" 13 | # fields = {} 14 | 15 | [flashduty] 16 | url = "https://api.flashcat.cloud/event/push/alert/standard?integration_key=x" 17 | timeout = "10s" 18 | -------------------------------------------------------------------------------- /conf.d/p.exec/exec.toml: -------------------------------------------------------------------------------- 1 | # script stdout example: 2 | # 1. It's an array 3 | # 2. event_status choice: Critical, Warning, Info, Ok 4 | # [ 5 | # { 6 | # "event_status": "Warning", 7 | # "labels": { 8 | # "check": "oom killed", 9 | # }, 10 | # "title_rule": "$check", 11 | # "description": "kernel: Out of memory: Kill process 9163 (mysqld) score 511 or sacrifice child" 12 | # } 13 | # ] 14 | 15 | [[instances]] 16 | # # commands, support glob 17 | commands = [ 18 | # "/opt/catpaw/scripts/*.sh" 19 | ] 20 | 21 | # # script timeout 22 | # timeout = "10s" 23 | 24 | # # Concurrent requests to make per target 25 | # concurrency = 5 26 | 27 | # # gather interval 28 | # interval = "30s" 29 | 30 | # # Optional append labels 31 | # labels = { env="production", team="devops" } 32 | 33 | [instances.alerting] 34 | ## Enable alerting or not 35 | enabled = true 36 | ## Same functionality as Prometheus keyword 'for' 37 | for_duration = 0 38 | ## Minimum interval duration between notifications 39 | repeat_interval = "5m" 40 | ## Maximum number of notifications 41 | repeat_number = 3 42 | ## Whether notify recovery event 43 | recovery_notification = true 44 | -------------------------------------------------------------------------------- /conf.d/p.filechange/filechange.toml: -------------------------------------------------------------------------------- 1 | [[instances]] 2 | time_span = "3m" 3 | filepaths = ["/etc/shadow"] 4 | check = "文件变化检测" 5 | interval = "30s" 6 | 7 | [instances.alerting] 8 | ## Enable alerting or not 9 | enabled = true 10 | ## Same functionality as Prometheus keyword 'for' 11 | for_duration = 0 12 | ## Minimum interval duration between notifications 13 | repeat_interval = "5m" 14 | ## Maximum number of notifications 15 | repeat_number = 3 16 | ## Whether notify recovery event 17 | recovery_notification = true 18 | ## Choice: Critical, Warning, Info 19 | default_severity = "Warning" 20 | -------------------------------------------------------------------------------- /conf.d/p.http/http.toml: -------------------------------------------------------------------------------- 1 | [[partials]] 2 | id = "default" 3 | 4 | # # Concurrent requests to make per instance 5 | # concurrency = 10 6 | 7 | ## Set http_proxy (catpaw uses the system wide proxy settings if it's is not set) 8 | # http_proxy = "http://localhost:8888" 9 | 10 | ## Interface to use when dialing an address 11 | # interface = "eth0" 12 | 13 | ## HTTP Request Method 14 | # method = "GET" 15 | 16 | ## Set timeout (default 5 seconds) 17 | # timeout = "5s" 18 | 19 | ## Whether to follow redirects from the server (defaults to false) 20 | # follow_redirects = false 21 | 22 | ## Optional HTTP Basic Auth Credentials 23 | # basic_auth_user = "username" 24 | # basic_auth_pass = "pa$$word" 25 | 26 | ## Optional headers 27 | # headers = ["Header-Key-1", "Header-Value-1", "Header-Key-2", "Header-Value-2"] 28 | 29 | ## Optional HTTP Request Body 30 | # payload = ''' 31 | # {'fake':'data'} 32 | # ''' 33 | 34 | [[instances]] 35 | targets = [ 36 | # "https://baidu.com", 37 | # "http://127.0.0.1:8888/request", 38 | ] 39 | 40 | partial = "default" 41 | 42 | # # gather interval 43 | # interval = "30s" 44 | 45 | # # Optional append labels 46 | # labels = { env="production", team="devops" } 47 | 48 | ## Optional TLS Config 49 | # use_tls = false 50 | # tls_ca = "/etc/catpaw/ca.pem" 51 | # tls_cert = "/etc/catpaw/cert.pem" 52 | # tls_key = "/etc/catpaw/key.pem" 53 | ## Use TLS but skip chain & host verification 54 | # insecure_skip_verify = false 55 | 56 | [instances.expect] 57 | ## Optional expected response status code. 58 | response_status_code = ["20*", "30*"] 59 | ## Optional substring match in body of the response (case sensitive) 60 | response_substring = "html" 61 | ## Optional alert when cert will expire in x hours 62 | cert_expire_threshold = "72h" 63 | 64 | [instances.alerting] 65 | ## Enable alerting or not 66 | enabled = true 67 | ## Same functionality as Prometheus keyword 'for' 68 | for_duration = 0 69 | ## Minimum interval duration between notifications 70 | repeat_interval = "5m" 71 | ## Maximum number of notifications 72 | repeat_number = 3 73 | ## Whether notify recovery event 74 | recovery_notification = true 75 | ## Choice: Critical, Warning, Info 76 | default_severity = "Warning" 77 | -------------------------------------------------------------------------------- /conf.d/p.journaltail/journaltail.toml: -------------------------------------------------------------------------------- 1 | [[instances]] 2 | # journalctl -S -${time_span} 3 | time_span = "1m" 4 | # relationship: or 5 | filter_include = ["*Out of memory*", "*nf_conntrack: table full, dropping packets*"] 6 | filter_exclude = [] 7 | # check rule name 8 | check = "Critical System Errors" 9 | # # gather interval 10 | interval = "30s" 11 | 12 | [instances.alerting] 13 | ## Enable alerting or not 14 | enabled = true 15 | ## Same functionality as Prometheus keyword 'for' 16 | for_duration = 0 17 | ## Minimum interval duration between notifications 18 | repeat_interval = "5m" 19 | ## Maximum number of notifications 20 | repeat_number = 3 21 | ## Whether notify recovery event 22 | recovery_notification = true 23 | ## Choice: Critical, Warning, Info 24 | default_severity = "Warning" 25 | -------------------------------------------------------------------------------- /conf.d/p.mtime/mtime.toml: -------------------------------------------------------------------------------- 1 | [[instances]] 2 | time_span = "3m" 3 | directory = "/tmp" 4 | check = "递归检测新文件或文件变化" 5 | interval = "30s" 6 | 7 | [instances.alerting] 8 | ## Enable alerting or not 9 | enabled = true 10 | ## Same functionality as Prometheus keyword 'for' 11 | for_duration = 0 12 | ## Minimum interval duration between notifications 13 | repeat_interval = "5m" 14 | ## Maximum number of notifications 15 | repeat_number = 3 16 | ## Whether notify recovery event 17 | recovery_notification = true 18 | ## Choice: Critical, Warning, Info 19 | default_severity = "Warning" 20 | -------------------------------------------------------------------------------- /conf.d/p.net/net.toml: -------------------------------------------------------------------------------- 1 | [[partials]] 2 | id = "default" 3 | 4 | # # Concurrent requests to make per instance 5 | # concurrency = 10 6 | 7 | ## Set connect timeout (default 5 seconds) 8 | # timeout = "1s" 9 | 10 | ## Set read timeout (only used if expecting a response) 11 | # read_timeout = "1s" 12 | 13 | # # Concurrent requests to make per instance 14 | # concurrency = 10 15 | 16 | ## Protocol, must be "tcp" or "udp" 17 | ## NOTE: because the "udp" protocol does not respond to requests, it requires 18 | ## a send/expect string pair (see below). 19 | # protocol = "tcp" 20 | 21 | ## The following options are required for UDP checks. For TCP, they are 22 | ## optional. The plugin will send the given string to the server and then 23 | ## expect to receive the given 'expect' string back. 24 | ## string sent to the server 25 | # send = "ssh" 26 | ## expected string in answer 27 | # expect = "ssh" 28 | 29 | 30 | [[instances]] 31 | targets = [ 32 | # "127.0.0.1:22", 33 | # "localhost:6379", 34 | # ":9090" 35 | ] 36 | 37 | partial = "default" 38 | 39 | # # gather interval 40 | # interval = "30s" 41 | 42 | # # Optional append labels 43 | # labels = { env="production", team="devops" } 44 | 45 | [instances.alerting] 46 | ## Enable alerting or not 47 | enabled = true 48 | ## Same functionality as Prometheus keyword 'for' 49 | for_duration = 0 50 | ## Minimum interval duration between notifications 51 | repeat_interval = "5m" 52 | ## Maximum number of notifications 53 | repeat_number = 3 54 | ## Whether notify recovery event 55 | recovery_notification = true 56 | ## Choice: Critical, Warning, Info 57 | default_severity = "Warning" 58 | -------------------------------------------------------------------------------- /conf.d/p.ping/ping.toml: -------------------------------------------------------------------------------- 1 | [[partials]] 2 | id = "default" 3 | 4 | # # Concurrent requests to make per instance 5 | # concurrency = 10 6 | 7 | ## Number of ping packets to send per interval. Corresponds to the "-c" 8 | ## option of the ping command. 9 | # count = 3 10 | 11 | ## Time to wait between sending ping packets in seconds. Operates like the 12 | ## "-i" option of the ping command. 13 | # ping_interval = 0.2 14 | 15 | ## If set, the time to wait for a ping response in seconds. Operates like 16 | ## the "-W" option of the ping command. 17 | # timeout = 2.0 18 | 19 | ## Interface or source address to send ping from. Operates like the -I or -S 20 | ## option of the ping command. 21 | # interface = "" 22 | 23 | ## Use only IPv6 addresses when resolving a hostname. 24 | # ipv6 = false 25 | 26 | ## Number of data bytes to be sent. Corresponds to the "-s" 27 | ## option of the ping command. 28 | # size = 56 29 | 30 | # alert if packet loss is above this threshold 31 | alert_if_packet_loss_percent_ge = 1.0 32 | 33 | [[instances]] 34 | targets = [ 35 | "127.0.0.1", 36 | ] 37 | 38 | partial = "default" 39 | 40 | # # gather interval 41 | # interval = "30s" 42 | 43 | # # Optional append labels 44 | # labels = { env="production", team="devops" } 45 | 46 | [instances.alerting] 47 | ## Enable alerting or not 48 | enabled = true 49 | ## Same functionality as Prometheus keyword 'for' 50 | for_duration = 0 51 | ## Minimum interval duration between notifications 52 | repeat_interval = "5m" 53 | ## Maximum number of notifications 54 | repeat_number = 3 55 | ## Whether notify recovery event 56 | recovery_notification = true 57 | ## Choice: Critical, Warning, Info 58 | default_severity = "Warning" 59 | -------------------------------------------------------------------------------- /conf.d/p.procnum/procnum.toml: -------------------------------------------------------------------------------- 1 | [[instances]] 2 | # # executable name (ie, pgrep ) 3 | # search_exec_substring = "" 4 | 5 | # # pattern as argument for pgrep (ie, pgrep -f ) 6 | search_cmdline_substring = "" 7 | 8 | # # windows service name 9 | # search_win_service = "" 10 | 11 | alert_if_num_lt = 1 12 | check = "进程存活检测(进程数量检测)" 13 | interval = "30s" 14 | 15 | [instances.alerting] 16 | ## Enable alerting or not 17 | enabled = true 18 | ## Same functionality as Prometheus keyword 'for' 19 | for_duration = 0 20 | ## Minimum interval duration between notifications 21 | repeat_interval = "5m" 22 | ## Maximum number of notifications 23 | repeat_number = 3 24 | ## Whether notify recovery event 25 | recovery_notification = true 26 | ## Choice: Critical, Warning, Info 27 | default_severity = "Warning" 28 | -------------------------------------------------------------------------------- /conf.d/p.sfilter/sfilter.toml: -------------------------------------------------------------------------------- 1 | [[instances]] 2 | # # e.g. /path/to/sfilter-scripts/system-health.sh 3 | command = "" 4 | # # script timeout 5 | timeout = "10s" 6 | # check rule name 7 | check = "Check system health state" 8 | 9 | # support glob 10 | filter_include = ["*WARNING*", "*CRITICAL*"] 11 | filter_exclude = [] 12 | 13 | # # gather interval 14 | interval = "30s" 15 | 16 | [instances.alerting] 17 | ## Enable alerting or not 18 | enabled = true 19 | ## Same functionality as Prometheus keyword 'for' 20 | for_duration = 0 21 | ## Minimum interval duration between notifications 22 | repeat_interval = "5m" 23 | ## Maximum number of notifications 24 | repeat_number = 3 25 | ## Whether notify recovery event 26 | recovery_notification = true 27 | ## Choice: Critical, Warning, Info 28 | default_severity = "Warning" 29 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strings" 10 | "time" 11 | 12 | "flashcat.cloud/catpaw/pkg/cfg" 13 | "github.com/jackpal/gateway" 14 | "github.com/toolkits/pkg/file" 15 | ) 16 | 17 | type Global struct { 18 | Interval Duration `toml:"interval"` 19 | Labels map[string]string `toml:"labels"` 20 | LabelHasHostname bool `toml:"label_has_hostname"` 21 | } 22 | 23 | type LogConfig struct { 24 | Level string `toml:"level"` 25 | Format string `toml:"format"` 26 | Output string `toml:"output"` 27 | Fields map[string]interface{} `toml:"fields"` 28 | } 29 | 30 | type Flashduty struct { 31 | Url string `toml:"url"` 32 | Timeout Duration `toml:"timeout"` 33 | Client *http.Client `toml:"-"` 34 | } 35 | 36 | type ConfigType struct { 37 | ConfigDir string `toml:"-"` 38 | TestMode bool `toml:"-"` 39 | Plugins string `toml:"-"` 40 | Url string `toml:"-"` 41 | Loglevel string `toml:"-"` 42 | 43 | Global Global `toml:"global"` 44 | LogConfig LogConfig `toml:"log"` 45 | Flashduty Flashduty `toml:"flashduty"` 46 | } 47 | 48 | var Config *ConfigType 49 | 50 | func InitConfig(configDir string, testMode bool, interval int64, plugins, url, loglevel string) error { 51 | configFile := path.Join(configDir, "config.toml") 52 | if !file.IsExist(configFile) { 53 | return fmt.Errorf("configuration file(%s) not found", configFile) 54 | } 55 | 56 | Config = &ConfigType{ 57 | ConfigDir: configDir, 58 | TestMode: testMode, 59 | Plugins: plugins, 60 | Url: url, 61 | Loglevel: loglevel, 62 | } 63 | 64 | if err := cfg.LoadConfigByDir(configDir, Config); err != nil { 65 | return fmt.Errorf("failed to load configs of directory: %s error:%s", configDir, err) 66 | } 67 | 68 | if interval > 0 { 69 | Config.Global.Interval = Duration(time.Second * time.Duration(interval)) 70 | } 71 | 72 | if Config.Global.Interval == 0 { 73 | Config.Global.Interval = Duration(30 * time.Second) 74 | } 75 | 76 | if Config.Loglevel != "" { 77 | Config.LogConfig.Level = Config.Loglevel 78 | } 79 | 80 | if Config.LogConfig.Level == "" { 81 | Config.LogConfig.Level = "info" 82 | } 83 | 84 | if Config.LogConfig.Format == "" { 85 | Config.LogConfig.Format = "json" 86 | } 87 | 88 | if len(Config.LogConfig.Output) == 0 { 89 | Config.LogConfig.Output = "stdout" 90 | } 91 | 92 | if Config.LogConfig.Fields == nil { 93 | Config.LogConfig.Fields = make(map[string]interface{}) 94 | } 95 | 96 | if Config.Flashduty.Timeout == 0 { 97 | Config.Flashduty.Timeout = Duration(10 * time.Second) 98 | } 99 | 100 | if Config.Url != "" { 101 | Config.Flashduty.Url = Config.Url 102 | } 103 | 104 | Config.Flashduty.Client = &http.Client{ 105 | Timeout: time.Duration(Config.Flashduty.Timeout), 106 | } 107 | 108 | if Config.Global.Labels == nil { 109 | Config.Global.Labels = make(map[string]string) 110 | } 111 | 112 | for k, v := range Config.Global.Labels { 113 | if !strings.Contains(v, "$") { 114 | continue 115 | } 116 | 117 | if strings.Contains(v, "$hostname") { 118 | Config.Global.LabelHasHostname = true 119 | continue 120 | } 121 | 122 | if strings.Contains(v, "$ip") { 123 | ip, err := GetOutboundIP() 124 | if err != nil { 125 | return fmt.Errorf("failed to get outbound ip: %v", err) 126 | } 127 | Config.Global.Labels[k] = strings.Replace(v, "$ip", fmt.Sprint(ip), -1) 128 | } 129 | 130 | Config.Global.Labels[k] = os.Expand(Config.Global.Labels[k], GetEnv) 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func GetEnv(key string) string { 137 | v := os.Getenv(key) 138 | var envVarEscaper = strings.NewReplacer( 139 | `"`, `\"`, 140 | `\`, `\\`, 141 | ) 142 | return envVarEscaper.Replace(v) 143 | } 144 | 145 | // Get preferred outbound ip of this machine 146 | func GetOutboundIP() (net.IP, error) { 147 | gateway, err := gateway.DiscoverGateway() 148 | if err != nil { 149 | return nil, fmt.Errorf("failed to detect gateway: %v", err) 150 | } 151 | 152 | gatewayip := fmt.Sprint(gateway) 153 | if gatewayip == "" { 154 | return nil, fmt.Errorf("failed to detect gateway: empty") 155 | } 156 | 157 | conn, err := net.Dial("udp", gatewayip+":80") 158 | if err != nil { 159 | return nil, fmt.Errorf("failed to get outbound ip: %v", err) 160 | } 161 | defer conn.Close() 162 | 163 | localAddr := conn.LocalAddr().(*net.UDPAddr) 164 | 165 | return localAddr.IP, nil 166 | } 167 | -------------------------------------------------------------------------------- /config/duration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Duration is a time.Duration 12 | type Duration time.Duration 13 | 14 | // UnmarshalTOML parses the duration from the TOML config file 15 | func (d *Duration) UnmarshalTOML(b []byte) error { 16 | // convert to string 17 | durStr := string(b) 18 | 19 | // Value is a TOML number (e.g. 3, 10, 3.5) 20 | // First try parsing as integer seconds 21 | sI, err := strconv.ParseInt(durStr, 10, 64) 22 | if err == nil { 23 | dur := time.Second * time.Duration(sI) 24 | *d = Duration(dur) 25 | return nil 26 | } 27 | // Second try parsing as float seconds 28 | sF, err := strconv.ParseFloat(durStr, 64) 29 | if err == nil { 30 | dur := time.Second * time.Duration(sF) 31 | *d = Duration(dur) 32 | return nil 33 | } 34 | 35 | // Finally, try value is a TOML string (e.g. "3s", 3s) or literal (e.g. '3s') 36 | durStr = strings.ReplaceAll(durStr, "'", "") 37 | durStr = strings.ReplaceAll(durStr, "\"", "") 38 | if durStr == "" { 39 | durStr = "0s" 40 | } 41 | 42 | dur, err := time.ParseDuration(durStr) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | *d = Duration(dur) 48 | return nil 49 | } 50 | 51 | func (d *Duration) UnmarshalText(text []byte) error { 52 | return d.UnmarshalTOML(text) 53 | } 54 | 55 | func (d *Duration) HumanString() string { 56 | duration := time.Duration(*d) 57 | if duration.Seconds() < 60.0 { 58 | return fmt.Sprintf("%d seconds", int64(duration.Seconds())) 59 | } 60 | if duration.Minutes() < 60.0 { 61 | remainingSeconds := math.Mod(duration.Seconds(), 60) 62 | return fmt.Sprintf("%d minutes %d seconds", int64(duration.Minutes()), int64(remainingSeconds)) 63 | } 64 | if duration.Hours() < 24.0 { 65 | remainingMinutes := math.Mod(duration.Minutes(), 60) 66 | remainingSeconds := math.Mod(duration.Seconds(), 60) 67 | return fmt.Sprintf("%d hours %d minutes %d seconds", 68 | int64(duration.Hours()), int64(remainingMinutes), int64(remainingSeconds)) 69 | } 70 | remainingHours := math.Mod(duration.Hours(), 24) 71 | remainingMinutes := math.Mod(duration.Minutes(), 60) 72 | remainingSeconds := math.Mod(duration.Seconds(), 60) 73 | return fmt.Sprintf("%d days %d hours %d minutes %d seconds", 74 | int64(duration.Hours()/24), int64(remainingHours), 75 | int64(remainingMinutes), int64(remainingSeconds)) 76 | } 77 | -------------------------------------------------------------------------------- /config/http_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | 9 | "flashcat.cloud/catpaw/pkg/tls" 10 | ) 11 | 12 | type HTTPConfig struct { 13 | Interface string `toml:"interface"` 14 | Method string `toml:"method"` 15 | Timeout Duration `toml:"timeout"` 16 | FollowRedirects *bool `toml:"follow_redirects"` 17 | BasicAuthUser string `toml:"basic_auth_user"` 18 | BasicAuthPass string `toml:"basic_auth_pass"` 19 | Headers []string `toml:"headers"` 20 | Payload string `toml:"payload"` 21 | HTTPProxy string `toml:"http_proxy"` 22 | tls.ClientConfig 23 | } 24 | 25 | type proxyFunc func(req *http.Request) (*url.URL, error) 26 | 27 | func (hc *HTTPConfig) GetProxy() (proxyFunc, error) { 28 | if len(hc.HTTPProxy) > 0 { 29 | address, err := url.Parse(hc.HTTPProxy) 30 | if err != nil { 31 | return nil, fmt.Errorf("error parsing proxy url %q: %w", hc.HTTPProxy, err) 32 | } 33 | return http.ProxyURL(address), nil 34 | } 35 | return http.ProxyFromEnvironment, nil 36 | } 37 | 38 | func (hc *HTTPConfig) GetTimeout() Duration { 39 | if hc.Timeout == 0 { 40 | return Duration(time.Second * 5) 41 | } 42 | return hc.Timeout 43 | } 44 | 45 | func (hc *HTTPConfig) GetMethod() string { 46 | if hc.Method == "" { 47 | return "GET" 48 | } 49 | return hc.Method 50 | } 51 | -------------------------------------------------------------------------------- /config/inline.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "flashcat.cloud/catpaw/types" 4 | 5 | type Alerting struct { 6 | Enabled bool `toml:"enabled"` 7 | 8 | // like prometheus `for` 9 | ForDuration Duration `toml:"for_duration"` 10 | 11 | // repeat interval 12 | RepeatInterval Duration `toml:"repeat_interval"` 13 | 14 | // maximum number of notifications 15 | RepeatNumber int `toml:"repeat_number"` 16 | 17 | // whether send recovery notification 18 | RecoveryNotification bool `toml:"recovery_notification"` 19 | 20 | // alert severity 21 | DefaultSeverity string `toml:"default_severity"` 22 | } 23 | 24 | type InternalConfig struct { 25 | // append labels to every event 26 | Labels map[string]string `toml:"labels"` 27 | 28 | // gather interval 29 | Interval Duration `toml:"interval"` 30 | 31 | // alerting rule 32 | Alerting Alerting `toml:"alerting"` 33 | 34 | // whether instance initialized 35 | initialized bool `toml:"-"` 36 | } 37 | 38 | func (ic *InternalConfig) GetLabels() map[string]string { 39 | if ic.Labels != nil { 40 | return ic.Labels 41 | } 42 | 43 | return map[string]string{} 44 | } 45 | 46 | func (ic *InternalConfig) GetInitialized() bool { 47 | return ic.initialized 48 | } 49 | 50 | func (ic *InternalConfig) SetInitialized() { 51 | ic.initialized = true 52 | } 53 | 54 | func (ic *InternalConfig) GetInterval() Duration { 55 | return ic.Interval 56 | } 57 | 58 | func (ic *InternalConfig) InitInternalConfig() error { 59 | // maybe compile some glob/regex pattern here 60 | return nil 61 | } 62 | 63 | func (ic *InternalConfig) GetAlerting() Alerting { 64 | return ic.Alerting 65 | } 66 | 67 | func (ic *InternalConfig) GetDefaultSeverity() string { 68 | if ic.Alerting.DefaultSeverity == "" { 69 | return types.EventStatusWarning 70 | } 71 | 72 | return ic.Alerting.DefaultSeverity 73 | } 74 | -------------------------------------------------------------------------------- /config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var Version = "0.8.0" 4 | -------------------------------------------------------------------------------- /docker/Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | FROM --platform=$TARGETPLATFORM ubuntu:23.04 2 | 3 | WORKDIR /app 4 | ADD catpaw /app 5 | # COPY conf.d /app/conf.d 6 | 7 | CMD ["/app/catpaw", "-h"] -------------------------------------------------------------------------------- /engine/cache.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "sync" 5 | 6 | "flashcat.cloud/catpaw/types" 7 | ) 8 | 9 | type EventCache struct { 10 | sync.RWMutex 11 | records map[string]*types.Event 12 | } 13 | 14 | var Events = &EventCache{records: make(map[string]*types.Event)} 15 | 16 | func (c *EventCache) Get(key string) *types.Event { 17 | c.RLock() 18 | defer c.RUnlock() 19 | return c.records[key] 20 | } 21 | 22 | func (c *EventCache) Set(val *types.Event) { 23 | c.Lock() 24 | defer c.Unlock() 25 | c.records[val.AlertKey] = val 26 | } 27 | 28 | func (c *EventCache) Del(key string) { 29 | c.Lock() 30 | defer c.Unlock() 31 | delete(c.records, key) 32 | } 33 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "sort" 11 | "strings" 12 | "time" 13 | 14 | "flashcat.cloud/catpaw/config" 15 | "flashcat.cloud/catpaw/logger" 16 | "flashcat.cloud/catpaw/pkg/safe" 17 | "flashcat.cloud/catpaw/plugins" 18 | "flashcat.cloud/catpaw/types" 19 | "github.com/toolkits/pkg/str" 20 | ) 21 | 22 | func PushRawEvents(pluginName string, pluginObj plugins.Plugin, ins plugins.Instance, queue *safe.Queue[*types.Event]) { 23 | if queue.Len() == 0 { 24 | return 25 | } 26 | 27 | now := time.Now().Unix() 28 | events := queue.PopBackAll() 29 | 30 | for i := range events { 31 | if events[i] == nil { 32 | continue 33 | } 34 | 35 | err := clean(events[i], now, pluginName, pluginObj, ins) 36 | if err != nil { 37 | logger.Logger.Errorf("clean raw event fail: %v, event: %v", err.Error(), events[i]) 38 | continue 39 | } 40 | 41 | logger.Logger.Debugf("event:%s: raw data received. event object: %v", events[i].AlertKey, events[i]) 42 | 43 | if !ins.GetAlerting().Enabled { 44 | continue 45 | } 46 | 47 | if events[i].EventStatus == types.EventStatusOk { 48 | handleRecoveryEvent(ins, events[i]) 49 | } else { 50 | handleAlertEvent(ins, events[i]) 51 | } 52 | } 53 | } 54 | 55 | // 处理恢复事件 56 | func handleRecoveryEvent(ins plugins.Instance, event *types.Event) { 57 | old := Events.Get(event.AlertKey) 58 | if old == nil { 59 | // 之前没有产生Event,当下的情况也是正常的,这是大多数场景,忽略即可,无需做任何处理 60 | return 61 | } 62 | 63 | // 之前产生了告警,现在恢复了,事件就可以从缓存删除了 64 | Events.Del(old.AlertKey) 65 | 66 | // 不过,也得看具体 alerting 的配置,如果不需要发送恢复通知,则忽略 67 | if ins.GetAlerting().RecoveryNotification && old.LastSent > 0 { 68 | event.LastSent = event.EventTime 69 | event.FirstFireTime = old.FirstFireTime 70 | event.NotifyCount = old.NotifyCount + 1 71 | forward(event) 72 | } 73 | } 74 | 75 | // 处理告警事件 76 | func handleAlertEvent(ins plugins.Instance, event *types.Event) { 77 | alerting := ins.GetAlerting() 78 | old := Events.Get(event.AlertKey) 79 | if old == nil { 80 | // 第一次产生告警事件 81 | event.FirstFireTime = event.EventTime 82 | 83 | // 无论如何,这个事件都得缓存起来 84 | Events.Set(event) 85 | 86 | // 要不要发?分两种情况。ForDuration 是 0 则立马发,否则等待 ForDuration 时间后再发 87 | if alerting.ForDuration == 0 { 88 | event.LastSent = event.EventTime 89 | event.NotifyCount++ 90 | forward(event) 91 | return 92 | } 93 | 94 | return 95 | } 96 | 97 | // old != nil 这已经不是第一次产生告警事件了 98 | // 如果 ForDuration 没有满足,则不能继续发送 99 | if alerting.ForDuration > 0 && event.EventTime-old.FirstFireTime < int64(alerting.ForDuration/config.Duration(time.Second)) { 100 | return 101 | } 102 | 103 | // ForDuration 满足了,可以继续发送了 104 | // 首先看是否达到最大发送次数 105 | if alerting.RepeatNumber > 0 && old.NotifyCount >= int64(alerting.RepeatNumber) { 106 | return 107 | } 108 | 109 | // 其次看发送频率,不能发的太快了 110 | if alerting.RepeatInterval > 0 && event.EventTime-old.LastSent < int64(alerting.RepeatInterval/config.Duration(time.Second)) { 111 | return 112 | } 113 | 114 | // 最后,可以发了 115 | event.LastSent = event.EventTime 116 | event.NotifyCount = old.NotifyCount + 1 117 | Events.Set(event) 118 | forward(event) 119 | } 120 | 121 | func clean(event *types.Event, now int64, pluginName string, pluginObj plugins.Plugin, ins plugins.Instance) error { 122 | if event.EventTime == 0 { 123 | event.EventTime = now 124 | } 125 | 126 | if !types.EventStatusValid(event.EventStatus) { 127 | return fmt.Errorf("invalid event_status: %s", event.EventStatus) 128 | } 129 | 130 | if event.Labels == nil { 131 | event.Labels = make(map[string]string) 132 | } 133 | 134 | // append label: from_plugin 135 | event.Labels["from_plugin"] = pluginName 136 | 137 | // append label from plugin 138 | plLabels := pluginObj.GetLabels() 139 | for k, v := range plLabels { 140 | event.Labels[k] = v 141 | } 142 | 143 | // append label from instance 144 | insLabels := ins.GetLabels() 145 | for k, v := range insLabels { 146 | event.Labels[k] = v 147 | } 148 | 149 | // append label: global labels 150 | var ( 151 | hostname string 152 | err error 153 | ) 154 | 155 | if config.Config.Global.LabelHasHostname { 156 | hostname, err = os.Hostname() 157 | if err != nil { 158 | return fmt.Errorf("failed to get hostname: %s", err.Error()) 159 | } 160 | } 161 | 162 | for key := range config.Config.Global.Labels { 163 | if strings.Contains(config.Config.Global.Labels[key], "$hostname") { 164 | event.Labels[key] = strings.ReplaceAll(config.Config.Global.Labels[key], "$hostname", hostname) 165 | } else { 166 | event.Labels[key] = config.Config.Global.Labels[key] 167 | } 168 | } 169 | 170 | count := len(event.Labels) 171 | keys := make([]string, 0, count) 172 | for k := range event.Labels { 173 | keys = append(keys, k) 174 | } 175 | 176 | sort.Strings(keys) 177 | 178 | var sb strings.Builder 179 | for _, k := range keys { 180 | sb.WriteString(k) 181 | sb.WriteString(":") 182 | sb.WriteString(event.Labels[k]) 183 | sb.WriteString(":") 184 | } 185 | 186 | event.AlertKey = str.MD5(sb.String()) 187 | 188 | return nil 189 | } 190 | 191 | func forward(event *types.Event) { 192 | if config.Config.TestMode { 193 | printStdout(event) 194 | return 195 | } 196 | 197 | bs, err := json.Marshal(event) 198 | if err != nil { 199 | logger.Logger.Errorf("event:%s: forward: marshal fail: %s", event.AlertKey, err.Error()) 200 | } 201 | 202 | req, err := http.NewRequest("POST", config.Config.Flashduty.Url, bytes.NewReader(bs)) 203 | if err != nil { 204 | logger.Logger.Errorf("event:%s: forward: new request fail: %s", event.AlertKey, err.Error()) 205 | return 206 | } 207 | 208 | req.Header.Set("Content-Type", "application/json") 209 | 210 | res, err := config.Config.Flashduty.Client.Do(req) 211 | if err != nil { 212 | logger.Logger.Errorf("event:%s: forward: do request fail: %s", event.AlertKey, err.Error()) 213 | return 214 | } 215 | 216 | var body []byte 217 | if res.Body != nil { 218 | defer res.Body.Close() 219 | body, err = io.ReadAll(res.Body) 220 | if err != nil { 221 | logger.Logger.Errorf("event:%s: forward: read response fail: %s", event.AlertKey, err.Error()) 222 | return 223 | } 224 | } 225 | 226 | logger.Logger.Infof("event:%s: forward: done, request payload: %s, response status code: %d, response body: %s", event.AlertKey, string(bs), res.StatusCode, string(body)) 227 | } 228 | 229 | func printStdout(event *types.Event) { 230 | var sb strings.Builder 231 | 232 | sb.WriteString(fmt.Sprint(event.EventTime)) 233 | sb.WriteString(" ") 234 | sb.WriteString(time.Unix(event.EventTime, 0).Format("15:04:05")) 235 | sb.WriteString(" ") 236 | sb.WriteString(event.AlertKey) 237 | sb.WriteString(" ") 238 | sb.WriteString(event.EventStatus) 239 | sb.WriteString(" ") 240 | 241 | for k, v := range event.Labels { 242 | sb.WriteString(fmt.Sprintf("%s=%s", k, v)) 243 | sb.WriteString(",") 244 | } 245 | 246 | sb.WriteString(" ") 247 | sb.WriteString(event.TitleRule) 248 | sb.WriteString(" ") 249 | sb.WriteString(event.Description) 250 | 251 | fmt.Println(sb.String()) 252 | } 253 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module flashcat.cloud/catpaw 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/chai2010/winsvc v0.0.0-20200705094454-db7ec320025c 7 | github.com/gobwas/glob v0.2.3 8 | github.com/jackpal/gateway v1.0.10 9 | github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7 10 | github.com/toolkits/pkg v1.3.3 11 | go.uber.org/zap v1.24.0 12 | ) 13 | 14 | require ( 15 | github.com/BurntSushi/toml v1.3.0 // indirect 16 | github.com/fatih/camelcase v1.0.0 // indirect 17 | github.com/fatih/structs v1.1.0 // indirect 18 | github.com/go-ole/go-ole v1.2.6 // indirect 19 | github.com/google/uuid v1.3.0 // indirect 20 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 21 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 22 | github.com/prometheus-community/pro-bing v0.2.0 // indirect 23 | github.com/shirou/gopsutil/v3 v3.23.6 // indirect 24 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 25 | github.com/tklauser/go-sysconf v0.3.11 // indirect 26 | github.com/tklauser/numcpus v0.6.0 // indirect 27 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 28 | go.uber.org/atomic v1.7.0 // indirect 29 | go.uber.org/automaxprocs v1.4.0 // indirect 30 | go.uber.org/multierr v1.6.0 // indirect 31 | golang.org/x/net v0.10.0 // indirect 32 | golang.org/x/sync v0.2.0 // indirect 33 | golang.org/x/sys v0.9.0 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.0 h1:Ws8e5YmnrGEHzZEzg0YvK/7COGYtTC5PbaH9oSSbgfA= 2 | github.com/BurntSushi/toml v1.3.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 4 | github.com/chai2010/winsvc v0.0.0-20200705094454-db7ec320025c h1:ZgxF2fGttmsetibm9Tc91TAUWzRZSfjJPstNxU6jWyU= 5 | github.com/chai2010/winsvc v0.0.0-20200705094454-db7ec320025c/go.mod h1:b9Xy0A0C/binZARjeVfHEr+gHzQUVztL71bTms7PRIM= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 10 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 11 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 12 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 13 | github.com/garyburd/redigo v1.6.2/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 14 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 15 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 16 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 17 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 18 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 19 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 20 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 21 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 22 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 23 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 24 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 25 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 28 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 29 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 30 | github.com/jackpal/gateway v1.0.10 h1:7g3fDo4Cd3RnTu6PzAfw6poO4Y81uNxrxFQFsBFSzJM= 31 | github.com/jackpal/gateway v1.0.10/go.mod h1:+uPBgIllrbkwYCAoDkGSZbjvpre/bGYAFCYIcrH+LHs= 32 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 34 | github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7 h1:SWlt7BoQNASbhTUD0Oy5yysI2seJ7vWuGUp///OM4TM= 35 | github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7/go.mod h1:Y2SaZf2Rzd0pXkLVhLlCiAXFCLSXAIbTKDivVgff/AM= 36 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 37 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 41 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 42 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 43 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 44 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 45 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 47 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 48 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 49 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 53 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 54 | github.com/prometheus-community/pro-bing v0.2.0 h1:hyK7yPFndU3LCDwEQJwPQUCjNkp1DGP/VxyzrWfXZUU= 55 | github.com/prometheus-community/pro-bing v0.2.0/go.mod h1:20arNb2S8rNG3EtmjHyZZU92cfbhQx7oCHZ9sulAV+I= 56 | github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY= 57 | github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHEHT08= 58 | github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU= 59 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 60 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 61 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 62 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 65 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 66 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 67 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 68 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 69 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 70 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 71 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 72 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 73 | github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= 74 | github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= 75 | github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= 76 | github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= 77 | github.com/toolkits/pkg v1.3.3 h1:qpQAQ18Jr47dv4NcBALlH0ad7L2PuqSh5K+nJKNg5lU= 78 | github.com/toolkits/pkg v1.3.3/go.mod h1:USXArTJlz1f1DCnQHNPYugO8GPkr1NRhP4eYQZQVshk= 79 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 80 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 81 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 82 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 83 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 84 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 85 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 86 | go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0= 87 | go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= 88 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 89 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 90 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 91 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 92 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 93 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 94 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 95 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 96 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 100 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 101 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 102 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 103 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 104 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 105 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= 108 | golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 120 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 122 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 124 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 125 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 126 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 127 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 128 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 129 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 130 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 131 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 132 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 137 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 140 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 141 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 142 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 144 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 145 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "time" 5 | 6 | "flashcat.cloud/catpaw/config" 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | var Logger *zap.SugaredLogger 12 | 13 | func Build() func() { 14 | c := config.Config.LogConfig 15 | 16 | loggerConfig := zap.NewProductionConfig() 17 | loggerConfig.EncoderConfig.TimeKey = "ts" 18 | loggerConfig.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339) 19 | loggerConfig.DisableStacktrace = true 20 | 21 | switch c.Level { 22 | case "debug": 23 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) 24 | case "info": 25 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) 26 | case "warn": 27 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.WarnLevel) 28 | case "error": 29 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) 30 | case "fatal": 31 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.FatalLevel) 32 | default: 33 | loggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) 34 | } 35 | 36 | loggerConfig.Encoding = c.Format 37 | loggerConfig.OutputPaths = []string{c.Output} 38 | loggerConfig.InitialFields = c.Fields 39 | 40 | logger, err := loggerConfig.Build() 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | Logger = logger.Sugar() 46 | 47 | return func() { Logger.Sync() } 48 | } 49 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "runtime" 9 | "syscall" 10 | 11 | "flashcat.cloud/catpaw/agent" 12 | "flashcat.cloud/catpaw/config" 13 | "flashcat.cloud/catpaw/logger" 14 | "flashcat.cloud/catpaw/winx" 15 | "github.com/chai2010/winsvc" 16 | "github.com/toolkits/pkg/runner" 17 | ) 18 | 19 | var ( 20 | appPath string 21 | configDir = flag.String("configs", "conf.d", "Specify configuration directory.") 22 | testMode = flag.Bool("test", false, "Is test mode? Print results to stdout if --test given.") 23 | interval = flag.Int64("interval", 0, "Global interval(unit:Second).") 24 | showVersion = flag.Bool("version", false, "Show version.") 25 | plugins = flag.String("plugins", "", "e.g. plugin1:plugin2") 26 | Url = flag.String("url", "", "e.g. https://api.flashcat.cloud/event/push/alert/standard?integration_key=x") 27 | Loglevel = flag.String("loglevel", "", "e.g. debug, info, warn, error, fatal") 28 | ) 29 | 30 | func init() { 31 | var err error 32 | if appPath, err = winsvc.GetAppPath(); err != nil { 33 | panic(err) 34 | } 35 | } 36 | 37 | func main() { 38 | flag.Parse() 39 | 40 | if *showVersion { 41 | fmt.Println(config.Version) 42 | os.Exit(0) 43 | } 44 | 45 | winx.Args(appPath) 46 | 47 | if err := config.InitConfig(*configDir, *testMode, *interval, *plugins, *Url, *Loglevel); err != nil { 48 | panic(err) 49 | } 50 | 51 | closefn := logger.Build() 52 | defer closefn() 53 | 54 | runner.Init() 55 | logger.Logger.Info("runner.binarydir: ", runner.Cwd) 56 | logger.Logger.Info("runner.configdir: ", *configDir) 57 | logger.Logger.Info("runner.hostname: ", runner.Hostname) 58 | logger.Logger.Info("runner.fd_limits: ", runner.FdLimits()) 59 | logger.Logger.Info("runner.vm_limits: ", runner.VMLimits()) 60 | 61 | agent := agent.New() 62 | 63 | if runtime.GOOS == "windows" && !winsvc.IsAnInteractiveSession() { 64 | if err := winsvc.RunAsService(winx.GetServiceName(), agent.Start, agent.Stop, false); err != nil { 65 | fmt.Println("failed to run windows service:", err) 66 | os.Exit(1) 67 | } 68 | return 69 | } else { 70 | agent.Start() 71 | } 72 | 73 | sc := make(chan os.Signal, 1) 74 | // syscall.SIGUSR2 == 0xc , not available on windows 75 | signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGPIPE) 76 | 77 | EXIT: 78 | for { 79 | sig := <-sc 80 | logger.Logger.Info("received signal: ", sig.String()) 81 | switch sig { 82 | case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: 83 | break EXIT 84 | case syscall.SIGHUP: 85 | agent.Reload() 86 | case syscall.SIGPIPE: 87 | // https://pkg.go.dev/os/signal#hdr-SIGPIPE 88 | // do nothing 89 | } 90 | } 91 | 92 | agent.Stop() 93 | logger.Logger.Info("agent exited") 94 | } 95 | -------------------------------------------------------------------------------- /pkg/cfg/cfg.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path" 7 | "strings" 8 | 9 | "github.com/koding/multiconfig" 10 | "github.com/toolkits/pkg/file" 11 | ) 12 | 13 | type ConfigFormat string 14 | 15 | const ( 16 | YamlFormat ConfigFormat = "yaml" 17 | TomlFormat ConfigFormat = "toml" 18 | JsonFormat ConfigFormat = "json" 19 | ) 20 | 21 | type ConfigWithFormat struct { 22 | Config string `json:"config"` 23 | Format ConfigFormat `json:"format"` 24 | checkSum string `json:"-"` 25 | } 26 | 27 | func (cwf *ConfigWithFormat) CheckSum() string { 28 | return cwf.checkSum 29 | } 30 | 31 | func (cwf *ConfigWithFormat) SetCheckSum(checkSum string) { 32 | cwf.checkSum = checkSum 33 | } 34 | 35 | func GuessFormat(fpath string) ConfigFormat { 36 | if strings.HasSuffix(fpath, ".json") { 37 | return JsonFormat 38 | } 39 | if strings.HasSuffix(fpath, ".yaml") || strings.HasSuffix(fpath, ".yml") { 40 | return YamlFormat 41 | } 42 | return TomlFormat 43 | } 44 | 45 | func LoadConfigByDir(configDir string, configPtr interface{}) error { 46 | var ( 47 | tBuf []byte 48 | ) 49 | 50 | loaders := []multiconfig.Loader{ 51 | &multiconfig.TagLoader{}, 52 | &multiconfig.EnvironmentLoader{}, 53 | } 54 | 55 | files, err := file.FilesUnder(configDir) 56 | if err != nil { 57 | return fmt.Errorf("failed to list files under: %s : %v", configDir, err) 58 | } 59 | s := NewFileScanner() 60 | for _, fpath := range files { 61 | switch { 62 | case strings.HasSuffix(fpath, ".toml"): 63 | s.Read(path.Join(configDir, fpath)) 64 | tBuf = append(tBuf, s.Data()...) 65 | tBuf = append(tBuf, []byte("\n")...) 66 | case strings.HasSuffix(fpath, ".json"): 67 | loaders = append(loaders, &multiconfig.JSONLoader{Path: path.Join(configDir, fpath)}) 68 | case strings.HasSuffix(fpath, ".yaml") || strings.HasSuffix(fpath, ".yml"): 69 | loaders = append(loaders, &multiconfig.YAMLLoader{Path: path.Join(configDir, fpath)}) 70 | } 71 | if s.Err() != nil { 72 | return s.Err() 73 | } 74 | } 75 | 76 | if len(tBuf) != 0 { 77 | loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader(tBuf)}) 78 | } 79 | 80 | m := multiconfig.DefaultLoader{ 81 | Loader: multiconfig.MultiLoader(loaders...), 82 | Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}), 83 | } 84 | return m.Load(configPtr) 85 | } 86 | 87 | func LoadConfigs(configs []ConfigWithFormat, configPtr interface{}) error { 88 | var ( 89 | tBuf, yBuf, jBuf []byte 90 | ) 91 | loaders := []multiconfig.Loader{ 92 | &multiconfig.TagLoader{}, 93 | &multiconfig.EnvironmentLoader{}, 94 | } 95 | for _, c := range configs { 96 | switch c.Format { 97 | case TomlFormat: 98 | tBuf = append(tBuf, []byte("\n\n")...) 99 | tBuf = append(tBuf, []byte(c.Config)...) 100 | case YamlFormat: 101 | yBuf = append(yBuf, []byte(c.Config)...) 102 | case JsonFormat: 103 | jBuf = append(jBuf, []byte(c.Config)...) 104 | } 105 | } 106 | 107 | if len(tBuf) != 0 { 108 | loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader(tBuf)}) 109 | } 110 | if len(yBuf) != 0 { 111 | loaders = append(loaders, &multiconfig.YAMLLoader{Reader: bytes.NewReader(yBuf)}) 112 | } 113 | if len(jBuf) != 0 { 114 | loaders = append(loaders, &multiconfig.JSONLoader{Reader: bytes.NewReader(jBuf)}) 115 | } 116 | 117 | m := multiconfig.DefaultLoader{ 118 | Loader: multiconfig.MultiLoader(loaders...), 119 | Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}), 120 | } 121 | return m.Load(configPtr) 122 | } 123 | 124 | func LoadSingleConfig(c ConfigWithFormat, configPtr interface{}) error { 125 | loaders := []multiconfig.Loader{ 126 | &multiconfig.TagLoader{}, 127 | &multiconfig.EnvironmentLoader{}, 128 | } 129 | 130 | switch c.Format { 131 | case TomlFormat: 132 | loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader([]byte(c.Config))}) 133 | case YamlFormat: 134 | loaders = append(loaders, &multiconfig.YAMLLoader{Reader: bytes.NewReader([]byte(c.Config))}) 135 | case JsonFormat: 136 | loaders = append(loaders, &multiconfig.JSONLoader{Reader: bytes.NewReader([]byte(c.Config))}) 137 | 138 | } 139 | 140 | m := multiconfig.DefaultLoader{ 141 | Loader: multiconfig.MultiLoader(loaders...), 142 | Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}), 143 | } 144 | return m.Load(configPtr) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/cfg/scan.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "io/ioutil" 5 | ) 6 | 7 | type scanner struct { 8 | data []byte 9 | err error 10 | } 11 | 12 | func NewFileScanner() *scanner { 13 | return &scanner{} 14 | } 15 | 16 | func (s *scanner) Err() error { 17 | return s.err 18 | } 19 | 20 | func (s *scanner) Data() []byte { 21 | return s.data 22 | } 23 | 24 | func (s *scanner) Read(file string) { 25 | if s.err == nil { 26 | s.data, s.err = ioutil.ReadFile(file) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/choice/choice.go: -------------------------------------------------------------------------------- 1 | // Package choice provides basic functions for working with 2 | // plugin options that must be one of several values. 3 | package choice 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // Contains return true if the choice in the list of choices. 11 | func Contains(choice string, choices []string) bool { 12 | for _, item := range choices { 13 | if item == choice { 14 | return true 15 | } 16 | } 17 | return false 18 | } 19 | 20 | // Contains return true if the choice in the list of choices. 21 | func ContainsPrefix(choice string, choices []string) bool { 22 | for _, item := range choices { 23 | if strings.HasPrefix(choice, item) { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | // Check returns an error if a choice is not one of 31 | // the available choices. 32 | func Check(choice string, available []string) error { 33 | if !Contains(choice, available) { 34 | return fmt.Errorf("unknown choice %s", choice) 35 | } 36 | return nil 37 | } 38 | 39 | // CheckSlice returns an error if the choices is not a subset of 40 | // available. 41 | func CheckSlice(choices, available []string) error { 42 | for _, choice := range choices { 43 | err := Check(choice, available) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/cmdx/cmd_notwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package cmdx 5 | 6 | import ( 7 | "os/exec" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | func CmdWait(cmd *exec.Cmd, timeout time.Duration) (error, bool) { 13 | var err error 14 | 15 | done := make(chan error) 16 | go func() { 17 | done <- cmd.Wait() 18 | }() 19 | 20 | select { 21 | case <-time.After(timeout): 22 | go func() { 23 | <-done // allow goroutine to exit 24 | }() 25 | 26 | // IMPORTANT: cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} is necessary before cmd.Start() 27 | err = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 28 | return err, true 29 | case err = <-done: 30 | return err, false 31 | } 32 | } 33 | 34 | func CmdStart(cmd *exec.Cmd) error { 35 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 36 | return cmd.Start() 37 | } 38 | -------------------------------------------------------------------------------- /pkg/cmdx/cmd_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package cmdx 5 | 6 | import ( 7 | "os/exec" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | func CmdWait(cmd *exec.Cmd, timeout time.Duration) (error, bool) { 13 | var err error 14 | 15 | done := make(chan error) 16 | go func() { 17 | done <- cmd.Wait() 18 | }() 19 | 20 | select { 21 | case <-time.After(timeout): 22 | go func() { 23 | <-done // allow goroutine to exit 24 | }() 25 | 26 | err = cmd.Process.Signal(syscall.SIGKILL) 27 | return err, true 28 | case err = <-done: 29 | return err, false 30 | } 31 | } 32 | 33 | func CmdStart(cmd *exec.Cmd) error { 34 | return cmd.Start() 35 | } 36 | -------------------------------------------------------------------------------- /pkg/cmdx/cmdx.go: -------------------------------------------------------------------------------- 1 | package cmdx 2 | 3 | import ( 4 | "os/exec" 5 | "time" 6 | ) 7 | 8 | func RunTimeout(cmd *exec.Cmd, timeout time.Duration) (error, bool) { 9 | err := CmdStart(cmd) 10 | if err != nil { 11 | return err, false 12 | } 13 | 14 | return CmdWait(cmd, timeout) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gobwas/glob" 7 | ) 8 | 9 | type Filter interface { 10 | Match(string) bool 11 | } 12 | 13 | // Compile takes a list of string filters and returns a Filter interface 14 | // for matching a given string against the filter list. The filter list 15 | // supports glob matching too, ie: 16 | // 17 | // f, _ := Compile([]string{"cpu", "mem", "net*"}) 18 | // f.Match("cpu") // true 19 | // f.Match("network") // true 20 | // f.Match("memory") // false 21 | func Compile(filters []string) (Filter, error) { 22 | // return if there is nothing to compile 23 | if len(filters) == 0 { 24 | return nil, nil 25 | } 26 | 27 | // check if we can compile a non-glob filter 28 | noGlob := true 29 | for _, filter := range filters { 30 | if HasMeta(filter) { 31 | noGlob = false 32 | break 33 | } 34 | } 35 | 36 | switch { 37 | case noGlob: 38 | // return non-globbing filter if not needed. 39 | return compileFilterNoGlob(filters), nil 40 | case len(filters) == 1: 41 | return glob.Compile(filters[0]) 42 | default: 43 | return glob.Compile("{" + strings.Join(filters, ",") + "}") 44 | } 45 | } 46 | 47 | // HasMeta reports whether path contains any magic glob characters. 48 | func HasMeta(s string) bool { 49 | return strings.ContainsAny(s, "*?[") 50 | } 51 | 52 | type filter struct { 53 | m map[string]struct{} 54 | } 55 | 56 | func (f *filter) Match(s string) bool { 57 | _, ok := f.m[s] 58 | return ok 59 | } 60 | 61 | type filtersingle struct { 62 | s string 63 | } 64 | 65 | func (f *filtersingle) Match(s string) bool { 66 | return f.s == s 67 | } 68 | 69 | func compileFilterNoGlob(filters []string) Filter { 70 | if len(filters) == 1 { 71 | return &filtersingle{s: filters[0]} 72 | } 73 | out := filter{m: make(map[string]struct{})} 74 | for _, filter := range filters { 75 | out.m[filter] = struct{}{} 76 | } 77 | return &out 78 | } 79 | 80 | type IncludeExcludeFilter struct { 81 | include Filter 82 | exclude Filter 83 | includeDefault bool 84 | excludeDefault bool 85 | } 86 | 87 | func NewIncludeExcludeFilter( 88 | include []string, 89 | exclude []string, 90 | ) (Filter, error) { 91 | return NewIncludeExcludeFilterDefaults(include, exclude, true, false) 92 | } 93 | 94 | func NewIncludeExcludeFilterDefaults( 95 | include []string, 96 | exclude []string, 97 | includeDefault bool, 98 | excludeDefault bool, 99 | ) (Filter, error) { 100 | in, err := Compile(include) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | ex, err := Compile(exclude) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | return &IncludeExcludeFilter{in, ex, includeDefault, excludeDefault}, nil 111 | } 112 | 113 | func (f *IncludeExcludeFilter) Match(s string) bool { 114 | if f.include != nil { 115 | if !f.include.Match(s) { 116 | return false 117 | } 118 | } else if !f.includeDefault { 119 | return false 120 | } 121 | 122 | if f.exclude != nil { 123 | if f.exclude.Match(s) { 124 | return false 125 | } 126 | } else if f.excludeDefault { 127 | return false 128 | } 129 | 130 | return true 131 | } 132 | -------------------------------------------------------------------------------- /pkg/netx/netx.go: -------------------------------------------------------------------------------- 1 | package netx 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | func LocalAddressByInterfaceName(interfaceName string) (net.Addr, error) { 9 | i, err := net.InterfaceByName(interfaceName) 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | addrs, err := i.Addrs() 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | for _, addr := range addrs { 20 | if naddr, ok := addr.(*net.IPNet); ok { 21 | // leaving port set to zero to let kernel pick 22 | return &net.TCPAddr{IP: naddr.IP}, nil 23 | } 24 | } 25 | 26 | return nil, fmt.Errorf("cannot create local address for interface %q", interfaceName) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/osx/osx.go: -------------------------------------------------------------------------------- 1 | package osx 2 | 3 | import "os" 4 | 5 | // getEnv returns the value of an environment variable, or returns the provided fallback value 6 | func GetEnv(key, fallback string) string { 7 | if value, ok := os.LookupEnv(key); ok { 8 | return value 9 | } 10 | return fallback 11 | } 12 | -------------------------------------------------------------------------------- /pkg/osx/proc.go: -------------------------------------------------------------------------------- 1 | package osx 2 | 3 | import "os" 4 | 5 | func GetHostProc() string { 6 | procPath := "/proc" 7 | if os.Getenv("HOST_PROC") != "" { 8 | procPath = os.Getenv("HOST_PROC") 9 | } 10 | return procPath 11 | } 12 | -------------------------------------------------------------------------------- /pkg/runtimex/stack.go: -------------------------------------------------------------------------------- 1 | package runtimex 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "runtime" 8 | ) 9 | 10 | var ( 11 | dunno = []byte("???") 12 | centerDot = []byte("·") 13 | dot = []byte(".") 14 | slash = []byte("/") 15 | ) 16 | 17 | // stack returns a nicely formatted stack frame, skipping skip frames. 18 | func Stack(skip int) []byte { 19 | buf := new(bytes.Buffer) // the returned data 20 | // As we loop, we open files and read them. These variables record the currently 21 | // loaded file. 22 | var lines [][]byte 23 | var lastFile string 24 | for i := skip; ; i++ { // Skip the expected number of frames 25 | pc, file, line, ok := runtime.Caller(i) 26 | if !ok { 27 | break 28 | } 29 | // Print this much at least. If we can't find the source, it won't show. 30 | fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) 31 | if file != lastFile { 32 | data, err := ioutil.ReadFile(file) 33 | if err != nil { 34 | continue 35 | } 36 | lines = bytes.Split(data, []byte{'\n'}) 37 | lastFile = file 38 | } 39 | fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) 40 | } 41 | return buf.Bytes() 42 | } 43 | 44 | // source returns a space-trimmed slice of the n'th line. 45 | func source(lines [][]byte, n int) []byte { 46 | n-- // in stack trace, lines are 1-indexed but our array is 0-indexed 47 | if n < 0 || n >= len(lines) { 48 | return dunno 49 | } 50 | return bytes.TrimSpace(lines[n]) 51 | } 52 | 53 | // function returns, if possible, the name of the function containing the PC. 54 | func function(pc uintptr) []byte { 55 | fn := runtime.FuncForPC(pc) 56 | if fn == nil { 57 | return dunno 58 | } 59 | name := []byte(fn.Name()) 60 | // The name includes the path name to the package, which is unnecessary 61 | // since the file name is already included. Plus, it has center dots. 62 | // That is, we see 63 | // runtime/debug.*T·ptrmethod 64 | // and want 65 | // *T.ptrmethod 66 | // Also the package path might contains dot (e.g. code.google.com/...), 67 | // so first eliminate the path prefix 68 | if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { 69 | name = name[lastslash+1:] 70 | } 71 | if period := bytes.Index(name, dot); period >= 0 { 72 | name = name[period+1:] 73 | } 74 | name = bytes.Replace(name, centerDot, dot, -1) 75 | return name 76 | } 77 | -------------------------------------------------------------------------------- /pkg/safe/queue.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | ) 7 | 8 | // Queue is a thread-safe linkedlist 9 | type Queue[T any] struct { 10 | sync.RWMutex 11 | linkedlist *list.List 12 | } 13 | 14 | func NewQueue[T any]() *Queue[T] { 15 | return &Queue[T]{linkedlist: list.New()} 16 | } 17 | 18 | func (q *Queue[T]) PushFront(v T) *list.Element { 19 | q.Lock() 20 | e := q.linkedlist.PushFront(v) 21 | q.Unlock() 22 | return e 23 | } 24 | 25 | func (q *Queue[T]) PushFrontN(vs []T) { 26 | q.Lock() 27 | for _, item := range vs { 28 | q.linkedlist.PushFront(item) 29 | } 30 | q.Unlock() 31 | } 32 | 33 | func (q *Queue[T]) PopBack() *T { 34 | q.Lock() 35 | defer q.Unlock() 36 | if elem := q.linkedlist.Back(); elem != nil { 37 | item := q.linkedlist.Remove(elem) 38 | v, ok := item.(T) 39 | if !ok { 40 | return nil 41 | } 42 | return &v 43 | } 44 | return nil 45 | } 46 | 47 | func (q *Queue[T]) PopBackN(n int) []T { 48 | q.Lock() 49 | defer q.Unlock() 50 | 51 | count := q.linkedlist.Len() 52 | if count == 0 { 53 | return nil 54 | } 55 | 56 | if count > n { 57 | count = n 58 | } 59 | 60 | items := make([]T, 0, count) 61 | for i := 0; i < count; i++ { 62 | data := q.linkedlist.Remove(q.linkedlist.Back()) 63 | item, ok := data.(T) 64 | if ok { 65 | items = append(items, item) 66 | } 67 | } 68 | return items 69 | } 70 | 71 | func (q *Queue[T]) PopBackAll() []T { 72 | q.Lock() 73 | defer q.Unlock() 74 | count := q.linkedlist.Len() 75 | if count == 0 { 76 | return nil 77 | } 78 | 79 | items := make([]T, 0, count) 80 | for i := 0; i < count; i++ { 81 | data := q.linkedlist.Remove(q.linkedlist.Back()) 82 | item, ok := data.(T) 83 | if ok { 84 | items = append(items, item) 85 | } 86 | } 87 | return items 88 | } 89 | 90 | func (q *Queue[T]) RemoveAll() { 91 | q.Lock() 92 | q.linkedlist.Init() 93 | q.Unlock() 94 | } 95 | 96 | func (q *Queue[T]) Len() int { 97 | q.RLock() 98 | size := q.linkedlist.Len() 99 | q.RUnlock() 100 | return size 101 | } 102 | 103 | // QueueLimited is Queue with Limited Size 104 | type QueueLimited[T any] struct { 105 | maxSize int 106 | queue *Queue[T] 107 | } 108 | 109 | func NewQueueLimited[T any](maxSize int) *QueueLimited[T] { 110 | return &QueueLimited[T]{queue: NewQueue[T](), maxSize: maxSize} 111 | } 112 | 113 | func (ql *QueueLimited[T]) PushFront(v T) bool { 114 | if ql.queue.Len() >= ql.maxSize { 115 | return false 116 | } 117 | 118 | ql.queue.PushFront(v) 119 | return true 120 | } 121 | 122 | func (ql *QueueLimited[T]) PushFrontN(vs []T) bool { 123 | if ql.queue.Len() >= ql.maxSize { 124 | return false 125 | } 126 | 127 | ql.queue.PushFrontN(vs) 128 | return true 129 | } 130 | 131 | func (ql *QueueLimited[T]) PopBack() *T { 132 | return ql.queue.PopBack() 133 | } 134 | 135 | func (ql *QueueLimited[T]) PopBackN(n int) []T { 136 | return ql.queue.PopBackN(n) 137 | } 138 | 139 | func (ql *QueueLimited[T]) PopBackAll() []T { 140 | return ql.queue.PopBackAll() 141 | } 142 | 143 | func (ql *QueueLimited[T]) RemoveAll() { 144 | ql.queue.RemoveAll() 145 | } 146 | 147 | func (ql *QueueLimited[T]) Len() int { 148 | return ql.queue.Len() 149 | } 150 | -------------------------------------------------------------------------------- /pkg/shell/shellquote.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "runtime" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | var ( 12 | UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string") 13 | UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string") 14 | UnterminatedEscapeError = errors.New("Unterminated backslash-escape") 15 | ) 16 | 17 | var ( 18 | splitChars = " \n\t" 19 | singleChar = '\'' 20 | doubleChar = '"' 21 | escapeChar = '\\' 22 | doubleEscapeChars = "$`\"\n\\" 23 | ) 24 | 25 | // Split splits a string according to /bin/sh's word-splitting rules. It 26 | // supports backslash-escapes, single-quotes, and double-quotes. Notably it does 27 | // not support the $” style of quoting. It also doesn't attempt to perform any 28 | // other sort of expansion, including brace expansion, shell expansion, or 29 | // pathname expansion. 30 | // 31 | // If the given input has an unterminated quoted string or ends in a 32 | // backslash-escape, one of UnterminatedSingleQuoteError, 33 | // UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned. 34 | func QuoteSplit(input string) (words []string, err error) { 35 | var buf bytes.Buffer 36 | words = make([]string, 0) 37 | 38 | for len(input) > 0 { 39 | // skip any splitChars at the start 40 | c, l := utf8.DecodeRuneInString(input) 41 | if strings.ContainsRune(splitChars, c) { 42 | input = input[l:] 43 | continue 44 | } else if c == escapeChar { 45 | // Look ahead for escaped newline so we can skip over it 46 | next := input[l:] 47 | if len(next) == 0 { 48 | err = UnterminatedEscapeError 49 | return 50 | } 51 | c2, l2 := utf8.DecodeRuneInString(next) 52 | if c2 == '\n' { 53 | input = next[l2:] 54 | continue 55 | } 56 | } 57 | 58 | var word string 59 | word, input, err = splitWord(input, &buf) 60 | if err != nil { 61 | return 62 | } 63 | words = append(words, word) 64 | } 65 | return 66 | } 67 | 68 | func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) { 69 | buf.Reset() 70 | 71 | raw: 72 | { 73 | cur := input 74 | for len(cur) > 0 { 75 | c, l := utf8.DecodeRuneInString(cur) 76 | cur = cur[l:] 77 | if c == singleChar { 78 | buf.WriteString(input[0 : len(input)-len(cur)-l]) 79 | input = cur 80 | goto single 81 | } else if c == doubleChar { 82 | buf.WriteString(input[0 : len(input)-len(cur)-l]) 83 | input = cur 84 | goto double 85 | } else if c == escapeChar { 86 | if runtime.GOOS == "windows" { 87 | buf.WriteString(input[0 : len(input)-len(cur)-l+1]) 88 | } else { 89 | buf.WriteString(input[0 : len(input)-len(cur)-l]) 90 | } 91 | input = cur 92 | goto escape 93 | } else if strings.ContainsRune(splitChars, c) { 94 | buf.WriteString(input[0 : len(input)-len(cur)-l]) 95 | return buf.String(), cur, nil 96 | } 97 | } 98 | if len(input) > 0 { 99 | buf.WriteString(input) 100 | input = "" 101 | } 102 | goto done 103 | } 104 | 105 | escape: 106 | { 107 | if len(input) == 0 { 108 | return "", "", UnterminatedEscapeError 109 | } 110 | c, l := utf8.DecodeRuneInString(input) 111 | if c == '\n' { 112 | // a backslash-escaped newline is elided from the output entirely 113 | } else { 114 | buf.WriteString(input[:l]) 115 | } 116 | input = input[l:] 117 | } 118 | goto raw 119 | 120 | single: 121 | { 122 | i := strings.IndexRune(input, singleChar) 123 | if i == -1 { 124 | return "", "", UnterminatedSingleQuoteError 125 | } 126 | buf.WriteString(input[0:i]) 127 | input = input[i+1:] 128 | goto raw 129 | } 130 | 131 | double: 132 | { 133 | cur := input 134 | for len(cur) > 0 { 135 | c, l := utf8.DecodeRuneInString(cur) 136 | cur = cur[l:] 137 | if c == doubleChar { 138 | buf.WriteString(input[0 : len(input)-len(cur)-l]) 139 | input = cur 140 | goto raw 141 | } else if c == escapeChar { 142 | // bash only supports certain escapes in double-quoted strings 143 | c2, l2 := utf8.DecodeRuneInString(cur) 144 | cur = cur[l2:] 145 | if strings.ContainsRune(doubleEscapeChars, c2) { 146 | buf.WriteString(input[0 : len(input)-len(cur)-l-l2]) 147 | if c2 == '\n' { 148 | // newline is special, skip the backslash entirely 149 | } else { 150 | buf.WriteRune(c2) 151 | } 152 | input = cur 153 | } 154 | } 155 | } 156 | return "", "", UnterminatedDoubleQuoteError 157 | } 158 | 159 | done: 160 | return buf.String(), input, nil 161 | } 162 | -------------------------------------------------------------------------------- /pkg/tls/common.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | ) 7 | 8 | var tlsVersionMap = map[string]uint16{ 9 | "TLS10": tls.VersionTLS10, 10 | "TLS11": tls.VersionTLS11, 11 | "TLS12": tls.VersionTLS12, 12 | "TLS13": tls.VersionTLS13, 13 | } 14 | 15 | var tlsCipherMap = map[string]uint16{ 16 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 17 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 18 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 19 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 20 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 21 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 22 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, 23 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 24 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, 25 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 26 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 27 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 28 | "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, 29 | "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 30 | "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, 31 | "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, 32 | "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, 33 | "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, 34 | "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, 35 | "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, 36 | "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, 37 | "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 38 | "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, 39 | "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, 40 | "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, 41 | } 42 | 43 | // ParseCiphers returns a `[]uint16` by received `[]string` key that represents ciphers from crypto/tls. 44 | // If some of ciphers in received list doesn't exists ParseCiphers returns nil with error 45 | func ParseCiphers(ciphers []string) ([]uint16, error) { 46 | suites := []uint16{} 47 | 48 | for _, cipher := range ciphers { 49 | v, ok := tlsCipherMap[cipher] 50 | if !ok { 51 | return nil, fmt.Errorf("unsupported cipher %q", cipher) 52 | } 53 | suites = append(suites, v) 54 | } 55 | 56 | return suites, nil 57 | } 58 | 59 | // ParseTLSVersion returns a `uint16` by received version string key that represents tls version from crypto/tls. 60 | // If version isn't supported ParseTLSVersion returns 0 with error 61 | func ParseTLSVersion(version string) (uint16, error) { 62 | if v, ok := tlsVersionMap[version]; ok { 63 | return v, nil 64 | } 65 | return 0, fmt.Errorf("unsupported version %q", version) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/tls/config.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "flashcat.cloud/catpaw/pkg/choice" 11 | ) 12 | 13 | // ClientConfig represents the standard client TLS config. 14 | type ClientConfig struct { 15 | UseTLS bool `toml:"use_tls"` 16 | TLSCA string `toml:"tls_ca"` 17 | TLSCert string `toml:"tls_cert"` 18 | TLSKey string `toml:"tls_key"` 19 | TLSKeyPwd string `toml:"tls_key_pwd"` 20 | InsecureSkipVerify bool `toml:"insecure_skip_verify"` 21 | ServerName string `toml:"tls_server_name"` 22 | TLSMinVersion string `toml:"tls_min_version"` 23 | TLSMaxVersion string `toml:"tls_max_version"` 24 | } 25 | 26 | // ServerConfig represents the standard server TLS config. 27 | type ServerConfig struct { 28 | TLSCert string `toml:"tls_cert"` 29 | TLSKey string `toml:"tls_key"` 30 | TLSKeyPwd string `toml:"tls_key_pwd"` 31 | TLSAllowedCACerts []string `toml:"tls_allowed_cacerts"` 32 | TLSCipherSuites []string `toml:"tls_cipher_suites"` 33 | TLSMinVersion string `toml:"tls_min_version"` 34 | TLSMaxVersion string `toml:"tls_max_version"` 35 | TLSAllowedDNSNames []string `toml:"tls_allowed_dns_names"` 36 | } 37 | 38 | // TLSConfig returns a tls.Config, may be nil without error if TLS is not 39 | // configured. 40 | func (c *ClientConfig) TLSConfig() (*tls.Config, error) { 41 | if !c.UseTLS { 42 | return nil, nil 43 | } 44 | 45 | tlsConfig := &tls.Config{ 46 | InsecureSkipVerify: c.InsecureSkipVerify, 47 | Renegotiation: tls.RenegotiateNever, 48 | } 49 | 50 | if c.TLSCA != "" { 51 | pool, err := makeCertPool([]string{c.TLSCA}) 52 | if err != nil { 53 | return nil, err 54 | } 55 | tlsConfig.RootCAs = pool 56 | } 57 | 58 | if c.TLSCert != "" && c.TLSKey != "" { 59 | err := loadCertificate(tlsConfig, c.TLSCert, c.TLSKey) 60 | if err != nil { 61 | return nil, err 62 | } 63 | } 64 | 65 | if c.ServerName != "" { 66 | tlsConfig.ServerName = c.ServerName 67 | } 68 | 69 | if c.TLSMinVersion == "1.0" { 70 | tlsConfig.MinVersion = tls.VersionTLS10 71 | } else if c.TLSMinVersion == "1.1" { 72 | tlsConfig.MinVersion = tls.VersionTLS11 73 | } else if c.TLSMinVersion == "1.2" { 74 | tlsConfig.MinVersion = tls.VersionTLS12 75 | } else if c.TLSMinVersion == "1.3" { 76 | tlsConfig.MinVersion = tls.VersionTLS13 77 | } 78 | 79 | if c.TLSMaxVersion == "1.0" { 80 | tlsConfig.MaxVersion = tls.VersionTLS10 81 | } else if c.TLSMaxVersion == "1.1" { 82 | tlsConfig.MaxVersion = tls.VersionTLS11 83 | } else if c.TLSMaxVersion == "1.2" { 84 | tlsConfig.MaxVersion = tls.VersionTLS12 85 | } else if c.TLSMaxVersion == "1.3" { 86 | tlsConfig.MaxVersion = tls.VersionTLS13 87 | } 88 | 89 | return tlsConfig, nil 90 | } 91 | 92 | // TLSConfig returns a tls.Config, may be nil without error if TLS is not 93 | // configured. 94 | func (c *ServerConfig) TLSConfig() (*tls.Config, error) { 95 | if c.TLSCert == "" && c.TLSKey == "" && len(c.TLSAllowedCACerts) == 0 { 96 | return nil, nil 97 | } 98 | 99 | tlsConfig := &tls.Config{} 100 | 101 | if len(c.TLSAllowedCACerts) != 0 { 102 | pool, err := makeCertPool(c.TLSAllowedCACerts) 103 | if err != nil { 104 | return nil, err 105 | } 106 | tlsConfig.ClientCAs = pool 107 | tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 108 | } 109 | 110 | if c.TLSCert != "" && c.TLSKey != "" { 111 | err := loadCertificate(tlsConfig, c.TLSCert, c.TLSKey) 112 | if err != nil { 113 | return nil, err 114 | } 115 | } 116 | 117 | if len(c.TLSCipherSuites) != 0 { 118 | cipherSuites, err := ParseCiphers(c.TLSCipherSuites) 119 | if err != nil { 120 | return nil, fmt.Errorf( 121 | "could not parse server cipher suites %s: %v", strings.Join(c.TLSCipherSuites, ","), err) 122 | } 123 | tlsConfig.CipherSuites = cipherSuites 124 | } 125 | 126 | if c.TLSMaxVersion != "" { 127 | version, err := ParseTLSVersion(c.TLSMaxVersion) 128 | if err != nil { 129 | return nil, fmt.Errorf( 130 | "could not parse tls max version %q: %v", c.TLSMaxVersion, err) 131 | } 132 | tlsConfig.MaxVersion = version 133 | } 134 | 135 | if c.TLSMinVersion != "" { 136 | version, err := ParseTLSVersion(c.TLSMinVersion) 137 | if err != nil { 138 | return nil, fmt.Errorf( 139 | "could not parse tls min version %q: %v", c.TLSMinVersion, err) 140 | } 141 | tlsConfig.MinVersion = version 142 | } 143 | 144 | if tlsConfig.MinVersion != 0 && tlsConfig.MaxVersion != 0 && tlsConfig.MinVersion > tlsConfig.MaxVersion { 145 | return nil, fmt.Errorf( 146 | "tls min version %q can't be greater than tls max version %q", tlsConfig.MinVersion, tlsConfig.MaxVersion) 147 | } 148 | 149 | // Since clientAuth is tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 150 | // there must be certs to validate. 151 | if len(c.TLSAllowedCACerts) > 0 && len(c.TLSAllowedDNSNames) > 0 { 152 | tlsConfig.VerifyPeerCertificate = c.verifyPeerCertificate 153 | } 154 | 155 | return tlsConfig, nil 156 | } 157 | 158 | func makeCertPool(certFiles []string) (*x509.CertPool, error) { 159 | pool := x509.NewCertPool() 160 | for _, certFile := range certFiles { 161 | pem, err := os.ReadFile(certFile) 162 | if err != nil { 163 | return nil, fmt.Errorf( 164 | "could not read certificate %q: %v", certFile, err) 165 | } 166 | if !pool.AppendCertsFromPEM(pem) { 167 | return nil, fmt.Errorf( 168 | "could not parse any PEM certificates %q: %v", certFile, err) 169 | } 170 | } 171 | return pool, nil 172 | } 173 | 174 | func loadCertificate(config *tls.Config, certFile, keyFile string) error { 175 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 176 | if err != nil { 177 | return fmt.Errorf( 178 | "could not load keypair %s:%s: %v", certFile, keyFile, err) 179 | } 180 | 181 | config.Certificates = []tls.Certificate{cert} 182 | config.BuildNameToCertificate() 183 | return nil 184 | } 185 | 186 | func (c *ServerConfig) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 187 | // The certificate chain is client + intermediate + root. 188 | // Let's review the client certificate. 189 | cert, err := x509.ParseCertificate(rawCerts[0]) 190 | if err != nil { 191 | return fmt.Errorf("could not validate peer certificate: %v", err) 192 | } 193 | 194 | for _, name := range cert.DNSNames { 195 | if choice.Contains(name, c.TLSAllowedDNSNames) { 196 | return nil 197 | } 198 | } 199 | 200 | return fmt.Errorf("peer certificate not in allowed DNS Name list: %v", cert.DNSNames) 201 | } 202 | -------------------------------------------------------------------------------- /plugins/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | osExec "os/exec" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "flashcat.cloud/catpaw/config" 16 | "flashcat.cloud/catpaw/logger" 17 | "flashcat.cloud/catpaw/pkg/cmdx" 18 | "flashcat.cloud/catpaw/pkg/safe" 19 | "flashcat.cloud/catpaw/pkg/shell" 20 | "flashcat.cloud/catpaw/plugins" 21 | "flashcat.cloud/catpaw/types" 22 | "github.com/toolkits/pkg/concurrent/semaphore" 23 | ) 24 | 25 | const ( 26 | pluginName string = "exec" 27 | maxStderrBytes int = 512 28 | ) 29 | 30 | type Instance struct { 31 | config.InternalConfig 32 | 33 | Commands []string `toml:"commands"` 34 | Timeout config.Duration `toml:"timeout"` 35 | Concurrency int `toml:"concurrency"` 36 | } 37 | 38 | type ExecPlugin struct { 39 | config.InternalConfig 40 | Instances []*Instance `toml:"instances"` 41 | } 42 | 43 | func (p *ExecPlugin) GetInstances() []plugins.Instance { 44 | ret := make([]plugins.Instance, len(p.Instances)) 45 | for i := 0; i < len(p.Instances); i++ { 46 | ret[i] = p.Instances[i] 47 | } 48 | return ret 49 | } 50 | 51 | func init() { 52 | plugins.Add(pluginName, func() plugins.Plugin { 53 | return &ExecPlugin{} 54 | }) 55 | } 56 | 57 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 58 | if len(ins.Commands) == 0 { 59 | return 60 | } 61 | 62 | if ins.Timeout == 0 { 63 | ins.Timeout = config.Duration(10 * time.Second) 64 | } 65 | 66 | if ins.Concurrency == 0 { 67 | ins.Concurrency = 5 68 | } 69 | 70 | var commands []string 71 | for _, pattern := range ins.Commands { 72 | cmdAndArgs := strings.SplitN(pattern, " ", 2) 73 | if len(cmdAndArgs) == 0 { 74 | continue 75 | } 76 | 77 | matches, err := filepath.Glob(cmdAndArgs[0]) 78 | if err != nil { 79 | logger.Logger.Errorw("failed to get filepath glob", "error", err, "pattern", cmdAndArgs[0]) 80 | continue 81 | } 82 | 83 | if len(matches) == 0 { 84 | // There were no matches with the glob pattern, so let's assume 85 | // that the command is in PATH and just run it as it is 86 | commands = append(commands, pattern) 87 | } else { 88 | // There were matches, so we'll append each match together with 89 | // the arguments to the commands slice 90 | for _, match := range matches { 91 | if len(cmdAndArgs) == 1 { 92 | commands = append(commands, match) 93 | } else { 94 | commands = append(commands, 95 | strings.Join([]string{match, cmdAndArgs[1]}, " ")) 96 | } 97 | } 98 | } 99 | } 100 | 101 | if len(commands) == 0 { 102 | logger.Logger.Warnln("no commands after parse") 103 | return 104 | } 105 | 106 | wg := new(sync.WaitGroup) 107 | se := semaphore.NewSemaphore(ins.Concurrency) 108 | for _, command := range commands { 109 | wg.Add(1) 110 | se.Acquire() 111 | go func(command string) { 112 | defer func() { 113 | se.Release() 114 | wg.Done() 115 | }() 116 | ins.gather(q, command) 117 | }(command) 118 | } 119 | wg.Wait() 120 | } 121 | 122 | func (ins *Instance) gather(q *safe.Queue[*types.Event], command string) { 123 | outbuf, errbuf, err := commandRun(command, time.Duration(ins.Timeout)) 124 | if err != nil || len(errbuf) > 0 { 125 | logger.Logger.Errorw("failed to exec command", "command", command, "error", err, "stderr", string(errbuf), "stdout", string(outbuf)) 126 | return 127 | } 128 | 129 | if len(outbuf) == 0 { 130 | logger.Logger.Warnw("exec command output is empty", "command", command) 131 | return 132 | } 133 | 134 | var events []*types.Event 135 | if err := json.Unmarshal(outbuf, &events); err != nil { 136 | logger.Logger.Errorw("failed to unmarshal command output", "command", command, "error", err, "output", string(outbuf)) 137 | return 138 | } 139 | 140 | for i := range events { 141 | q.PushFront(events[i]) 142 | } 143 | } 144 | 145 | func commandRun(command string, timeout time.Duration) ([]byte, []byte, error) { 146 | splitCmd, err := shell.QuoteSplit(command) 147 | if err != nil || len(splitCmd) == 0 { 148 | return nil, nil, fmt.Errorf("exec: unable to parse command, %s", err) 149 | } 150 | 151 | cmd := osExec.Command(splitCmd[0], splitCmd[1:]...) 152 | 153 | var ( 154 | out bytes.Buffer 155 | stderr bytes.Buffer 156 | ) 157 | cmd.Stdout = &out 158 | cmd.Stderr = &stderr 159 | 160 | runError, runTimeout := cmdx.RunTimeout(cmd, timeout) 161 | if runTimeout { 162 | return nil, nil, fmt.Errorf("exec %s timeout", command) 163 | } 164 | 165 | out = removeWindowsCarriageReturns(out) 166 | if stderr.Len() > 0 { 167 | stderr = removeWindowsCarriageReturns(stderr) 168 | stderr = truncate(stderr) 169 | } 170 | 171 | return out.Bytes(), stderr.Bytes(), runError 172 | } 173 | 174 | func truncate(buf bytes.Buffer) bytes.Buffer { 175 | // Limit the number of bytes. 176 | didTruncate := false 177 | if buf.Len() > maxStderrBytes { 178 | buf.Truncate(maxStderrBytes) 179 | didTruncate = true 180 | } 181 | if i := bytes.IndexByte(buf.Bytes(), '\n'); i > 0 { 182 | // Only show truncation if the newline wasn't the last character. 183 | if i < buf.Len()-1 { 184 | didTruncate = true 185 | } 186 | buf.Truncate(i) 187 | } 188 | if didTruncate { 189 | //nolint:errcheck,revive // Will always return nil or panic 190 | buf.WriteString("...") 191 | } 192 | return buf 193 | } 194 | 195 | // removeWindowsCarriageReturns removes all carriage returns from the input if the 196 | // OS is Windows. It does not return any errors. 197 | func removeWindowsCarriageReturns(b bytes.Buffer) bytes.Buffer { 198 | if runtime.GOOS == "windows" { 199 | var buf bytes.Buffer 200 | for { 201 | byt, err := b.ReadBytes(0x0D) 202 | byt = bytes.TrimRight(byt, "\x0d") 203 | if len(byt) > 0 { 204 | _, _ = buf.Write(byt) 205 | } 206 | if err == io.EOF { 207 | return buf 208 | } 209 | } 210 | } 211 | return b 212 | } 213 | -------------------------------------------------------------------------------- /plugins/filechange/filechange.go: -------------------------------------------------------------------------------- 1 | package filechange 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "flashcat.cloud/catpaw/config" 11 | "flashcat.cloud/catpaw/logger" 12 | "flashcat.cloud/catpaw/pkg/safe" 13 | "flashcat.cloud/catpaw/plugins" 14 | "flashcat.cloud/catpaw/types" 15 | ) 16 | 17 | const ( 18 | pluginName string = "filechange" 19 | ) 20 | 21 | type Instance struct { 22 | config.InternalConfig 23 | 24 | TimeSpan time.Duration `toml:"time_span"` 25 | Filepaths []string `toml:"filepaths"` 26 | Check string `toml:"check"` 27 | } 28 | 29 | type FileChangePlugin struct { 30 | config.InternalConfig 31 | Instances []*Instance `toml:"instances"` 32 | } 33 | 34 | func (p *FileChangePlugin) GetInstances() []plugins.Instance { 35 | ret := make([]plugins.Instance, len(p.Instances)) 36 | for i := 0; i < len(p.Instances); i++ { 37 | ret[i] = p.Instances[i] 38 | } 39 | return ret 40 | } 41 | 42 | func init() { 43 | plugins.Add(pluginName, func() plugins.Plugin { 44 | return &FileChangePlugin{} 45 | }) 46 | } 47 | 48 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 49 | if ins.TimeSpan == 0 { 50 | ins.TimeSpan = 3 * time.Minute 51 | } 52 | 53 | if ins.Check == "" { 54 | logger.Logger.Error("check is empty") 55 | return 56 | } 57 | 58 | if len(ins.Filepaths) == 0 { 59 | logger.Logger.Error("filepaths is empty") 60 | return 61 | } 62 | 63 | // get all files 64 | var fps []string 65 | for _, fp := range ins.Filepaths { 66 | matches, err := filepath.Glob(fp) 67 | if err != nil { 68 | logger.Logger.Errorf("glob %s error: %v", fp, err) 69 | continue 70 | } 71 | 72 | if len(matches) == 0 { 73 | continue 74 | } 75 | 76 | fps = append(fps, matches...) 77 | } 78 | 79 | // check mtime 80 | now := time.Now() 81 | files := make(map[string]time.Time) 82 | 83 | for _, fp := range fps { 84 | f, e := os.Stat(fp) 85 | if e != nil { 86 | logger.Logger.Errorf("stat %s error: %v", fp, e) 87 | continue 88 | } 89 | 90 | mtime := f.ModTime() 91 | if now.Sub(mtime) < ins.TimeSpan { 92 | files[fp] = mtime 93 | } 94 | } 95 | 96 | if len(files) == 0 { 97 | q.PushFront(ins.buildEvent(ins.Check, "files not changed")) 98 | return 99 | } 100 | 101 | var body strings.Builder 102 | body.WriteString(head) 103 | 104 | for fp, mtime := range files { 105 | body.WriteString("| ") 106 | body.WriteString(fp) 107 | body.WriteString(" | ") 108 | body.WriteString(mtime.Format("2006-01-02 15:04:05")) 109 | body.WriteString(" |\n") 110 | } 111 | 112 | title := fmt.Sprintf("files changed\n\nconfiguration.filepaths:`%s`\n", ins.Filepaths) 113 | q.PushFront(ins.buildEvent(ins.Check, "[MD]", title, body.String()).SetEventStatus(ins.GetDefaultSeverity())) 114 | } 115 | 116 | func (ins *Instance) buildEvent(check string, desc ...string) *types.Event { 117 | event := types.BuildEvent(map[string]string{"check": check}).SetTitleRule("$check") 118 | if len(desc) > 0 { 119 | event.SetDescription(strings.Join(desc, "\n")) 120 | } 121 | return event 122 | } 123 | 124 | var head = `| File | MTime | 125 | | :--| --: | 126 | ` 127 | -------------------------------------------------------------------------------- /plugins/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "flashcat.cloud/catpaw/config" 14 | "flashcat.cloud/catpaw/logger" 15 | "flashcat.cloud/catpaw/pkg/filter" 16 | "flashcat.cloud/catpaw/pkg/netx" 17 | "flashcat.cloud/catpaw/pkg/safe" 18 | "flashcat.cloud/catpaw/plugins" 19 | "flashcat.cloud/catpaw/types" 20 | "github.com/toolkits/pkg/concurrent/semaphore" 21 | ) 22 | 23 | const pluginName = "http" 24 | 25 | type Expect struct { 26 | ResponseSubstring string `toml:"response_substring"` 27 | ResponseStatusCode []string `toml:"response_status_code"` 28 | ResponseStatusCodeFilter filter.Filter `toml:"-"` // compiled filter 29 | CertExpireThreshold config.Duration `toml:"cert_expire_threshold"` 30 | } 31 | 32 | type Partial struct { 33 | ID string `toml:"id"` 34 | Concurrency int `toml:"concurrency"` 35 | config.HTTPConfig 36 | } 37 | 38 | type Instance struct { 39 | config.InternalConfig 40 | Partial string `toml:"partial"` 41 | 42 | Targets []string `toml:"targets"` 43 | Concurrency int `toml:"concurrency"` 44 | Expect Expect `toml:"expect"` 45 | 46 | config.HTTPConfig 47 | client httpClient 48 | } 49 | 50 | type HttpPlugin struct { 51 | config.InternalConfig 52 | Partials []Partial `toml:"partials"` 53 | Instances []*Instance `toml:"instances"` 54 | } 55 | 56 | func (p *HttpPlugin) ApplyPartials() error { 57 | for i := 0; i < len(p.Instances); i++ { 58 | id := p.Instances[i].Partial 59 | if id != "" { 60 | for _, partial := range p.Partials { 61 | if partial.ID == id { 62 | // use partial config as default 63 | if p.Instances[i].Concurrency == 0 { 64 | p.Instances[i].Concurrency = partial.Concurrency 65 | } 66 | 67 | if p.Instances[i].HTTPConfig.HTTPProxy == "" { 68 | p.Instances[i].HTTPConfig.HTTPProxy = partial.HTTPProxy 69 | } 70 | 71 | if p.Instances[i].HTTPConfig.Interface == "" { 72 | p.Instances[i].HTTPConfig.Interface = partial.Interface 73 | } 74 | 75 | if p.Instances[i].HTTPConfig.Method == "" { 76 | p.Instances[i].HTTPConfig.Method = partial.Method 77 | } 78 | 79 | if p.Instances[i].HTTPConfig.Timeout == 0 { 80 | p.Instances[i].HTTPConfig.Timeout = partial.Timeout 81 | } 82 | 83 | if p.Instances[i].HTTPConfig.FollowRedirects == nil { 84 | p.Instances[i].HTTPConfig.FollowRedirects = partial.FollowRedirects 85 | } 86 | 87 | if p.Instances[i].HTTPConfig.BasicAuthUser == "" { 88 | p.Instances[i].HTTPConfig.BasicAuthUser = partial.BasicAuthUser 89 | } 90 | 91 | if p.Instances[i].HTTPConfig.BasicAuthPass == "" { 92 | p.Instances[i].HTTPConfig.BasicAuthPass = partial.BasicAuthPass 93 | } 94 | 95 | if len(p.Instances[i].HTTPConfig.Headers) == 0 { 96 | p.Instances[i].HTTPConfig.Headers = partial.Headers 97 | } 98 | 99 | if p.Instances[i].HTTPConfig.Payload == "" { 100 | p.Instances[i].HTTPConfig.Payload = partial.Payload 101 | } 102 | 103 | break 104 | } 105 | } 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | func init() { 112 | plugins.Add(pluginName, func() plugins.Plugin { 113 | return &HttpPlugin{} 114 | }) 115 | } 116 | 117 | type httpClient interface { 118 | Do(req *http.Request) (*http.Response, error) 119 | } 120 | 121 | func (ins *Instance) Init() error { 122 | if ins.Concurrency == 0 { 123 | ins.Concurrency = 10 124 | } 125 | 126 | var err error 127 | if len(ins.Expect.ResponseStatusCode) > 0 { 128 | ins.Expect.ResponseStatusCodeFilter, err = filter.Compile(ins.Expect.ResponseStatusCode) 129 | if err != nil { 130 | return err 131 | } 132 | } 133 | 134 | client, err := ins.createHTTPClient() 135 | if err != nil { 136 | return fmt.Errorf("failed to create http client: %v", err) 137 | } 138 | 139 | ins.client = client 140 | 141 | for _, target := range ins.Targets { 142 | addr, err := url.Parse(target) 143 | if err != nil { 144 | return fmt.Errorf("failed to parse http url: %s, error: %v", target, err) 145 | } 146 | 147 | if addr.Scheme != "http" && addr.Scheme != "https" { 148 | return fmt.Errorf("only http and https are supported, target: %s", target) 149 | } 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func (ins *Instance) createHTTPClient() (*http.Client, error) { 156 | tlsCfg, err := ins.ClientConfig.TLSConfig() 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | dialer := &net.Dialer{} 162 | 163 | if ins.Interface != "" { 164 | dialer.LocalAddr, err = netx.LocalAddressByInterfaceName(ins.Interface) 165 | if err != nil { 166 | return nil, err 167 | } 168 | } 169 | 170 | proxy, err := ins.GetProxy() 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | trans := &http.Transport{ 176 | Proxy: proxy, 177 | DialContext: dialer.DialContext, 178 | DisableKeepAlives: true, 179 | TLSClientConfig: tlsCfg, 180 | } 181 | 182 | if ins.UseTLS { 183 | trans.TLSClientConfig = tlsCfg 184 | } 185 | 186 | client := &http.Client{ 187 | Transport: trans, 188 | Timeout: time.Duration(ins.GetTimeout()), 189 | } 190 | 191 | if ins.FollowRedirects != nil && *ins.FollowRedirects { 192 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 193 | if len(via) >= 10 { 194 | return fmt.Errorf("stopped after 10 redirects") 195 | } 196 | return http.ErrUseLastResponse 197 | } 198 | } 199 | 200 | return client, nil 201 | } 202 | 203 | func (h *HttpPlugin) GetInstances() []plugins.Instance { 204 | ret := make([]plugins.Instance, len(h.Instances)) 205 | for i := 0; i < len(h.Instances); i++ { 206 | ret[i] = h.Instances[i] 207 | } 208 | return ret 209 | } 210 | 211 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 212 | if len(ins.Targets) == 0 { 213 | return 214 | } 215 | 216 | if !ins.GetInitialized() { 217 | if err := ins.Init(); err != nil { 218 | logger.Logger.Errorf("failed to init http plugin instance: %v", err) 219 | return 220 | } else { 221 | ins.SetInitialized() 222 | } 223 | } 224 | 225 | wg := new(sync.WaitGroup) 226 | se := semaphore.NewSemaphore(ins.Concurrency) 227 | for _, target := range ins.Targets { 228 | wg.Add(1) 229 | se.Acquire() 230 | go func(target string) { 231 | defer func() { 232 | se.Release() 233 | wg.Done() 234 | }() 235 | ins.gather(q, target) 236 | }(target) 237 | } 238 | wg.Wait() 239 | } 240 | 241 | func (ins *Instance) gather(q *safe.Queue[*types.Event], target string) { 242 | logger.Logger.Debug("http target: ", target) 243 | 244 | labels := map[string]string{ 245 | "target": target, 246 | "method": ins.GetMethod(), 247 | } 248 | 249 | var payload io.Reader 250 | if ins.Payload != "" { 251 | payload = strings.NewReader(ins.Payload) 252 | } 253 | 254 | request, err := http.NewRequest(ins.Method, target, payload) 255 | if err != nil { 256 | logger.Logger.Errorw("failed to create http request", "error", err, "plugin", pluginName) 257 | return 258 | } 259 | 260 | for i := 0; i < len(ins.Headers); i += 2 { 261 | request.Header.Add(ins.Headers[i], ins.Headers[i+1]) 262 | if ins.Headers[i] == "Host" { 263 | request.Host = ins.Headers[i+1] 264 | } 265 | } 266 | 267 | if ins.BasicAuthUser != "" || ins.BasicAuthPass != "" { 268 | request.SetBasicAuth(ins.BasicAuthUser, ins.BasicAuthPass) 269 | } 270 | 271 | // check connection 272 | resp, err := ins.client.Do(request) 273 | 274 | e := types.BuildEvent(map[string]string{ 275 | "check": "HTTP check failed", 276 | }, labels) 277 | 278 | errString := "null. everything is ok" 279 | if err != nil { 280 | e.SetEventStatus(ins.GetDefaultSeverity()) 281 | errString = err.Error() 282 | } 283 | 284 | e.SetTitleRule("$check").SetDescription(`[MD] 285 | - **target**: ` + target + ` 286 | - **method**: ` + ins.GetMethod() + ` 287 | - **error**: ` + errString + ` 288 | `) 289 | 290 | q.PushFront(e) 291 | 292 | if err != nil { 293 | logger.Logger.Errorw("failed to send http request", "error", err, "plugin", pluginName, "target", target) 294 | return 295 | } 296 | 297 | // check tls cert 298 | if ins.Expect.CertExpireThreshold > 0 && strings.HasPrefix(target, "https://") && resp.TLS != nil { 299 | e := types.BuildEvent(map[string]string{ 300 | "check": "TLS cert will expire soon", 301 | }, labels) 302 | 303 | certExpireTimestamp := getEarliestCertExpiry(resp.TLS).Unix() 304 | if certExpireTimestamp < time.Now().Add(time.Duration(ins.Expect.CertExpireThreshold)).Unix() { 305 | e.SetEventStatus(ins.GetDefaultSeverity()) 306 | } 307 | 308 | e.SetTitleRule("$check").SetDescription(`[MD] 309 | - **target**: ` + target + ` 310 | - **method**: ` + ins.GetMethod() + ` 311 | - **cert expire threshold**: ` + ins.Expect.CertExpireThreshold.HumanString() + ` 312 | - **cert expire at**: ` + time.Unix(certExpireTimestamp, 0).Format("2006-01-02 15:04:05") + ` 313 | `) 314 | 315 | q.PushFront(e) 316 | } 317 | 318 | var body []byte 319 | if resp.Body != nil { 320 | defer resp.Body.Close() 321 | body, err = io.ReadAll(resp.Body) 322 | if err != nil { 323 | logger.Logger.Errorw("failed to read http response body", "error", err, "plugin", pluginName, "target", target) 324 | return 325 | } 326 | } 327 | 328 | statusCode := fmt.Sprint(resp.StatusCode) 329 | 330 | if len(ins.Expect.ResponseStatusCode) > 0 { 331 | e := types.BuildEvent(map[string]string{ 332 | "check": "HTTP response status code not match", 333 | }, labels) 334 | 335 | if !ins.Expect.ResponseStatusCodeFilter.Match(statusCode) { 336 | e.SetEventStatus(ins.GetDefaultSeverity()) 337 | } 338 | 339 | e.SetTitleRule("$check").SetDescription(fmt.Sprintf(ExpectResponseStatusCodeDesn, target, ins.GetMethod(), statusCode, ins.Expect.ResponseStatusCode, string(body))) 340 | q.PushFront(e) 341 | } 342 | 343 | if len(ins.Expect.ResponseSubstring) > 0 { 344 | e := types.BuildEvent(map[string]string{ 345 | "check": "HTTP response body not match", 346 | }, labels) 347 | 348 | if !strings.Contains(string(body), ins.Expect.ResponseSubstring) { 349 | e.SetEventStatus(ins.GetDefaultSeverity()) 350 | } 351 | 352 | e.SetTitleRule("$check").SetDescription(fmt.Sprintf(ExpectResponseSubstringDesn, target, ins.GetMethod(), statusCode, ins.Expect.ResponseSubstring, string(body))) 353 | q.PushFront(e) 354 | } 355 | } 356 | 357 | var ExpectResponseStatusCodeDesn = `[MD] 358 | - **target**: %s 359 | - **method**: %s 360 | - **status code**: %s 361 | - **expect code**: %v 362 | 363 | **response body**: 364 | 365 | ` + "```" + ` 366 | %s 367 | ` + "```" + ` 368 | ` 369 | 370 | var ExpectResponseSubstringDesn = `[MD] 371 | - **target**: %s 372 | - **method**: %s 373 | - **status code**: %s 374 | - **expect substring**: %v 375 | 376 | **response body**: 377 | 378 | ` + "```" + ` 379 | %s 380 | ` + "```" + ` 381 | ` 382 | -------------------------------------------------------------------------------- /plugins/http/tls.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "crypto/tls" 5 | "time" 6 | ) 7 | 8 | func getEarliestCertExpiry(state *tls.ConnectionState) time.Time { 9 | earliest := time.Time{} 10 | for _, cert := range state.PeerCertificates { 11 | if (earliest.IsZero() || cert.NotAfter.Before(earliest)) && !cert.NotAfter.IsZero() { 12 | earliest = cert.NotAfter 13 | } 14 | } 15 | return earliest 16 | } 17 | -------------------------------------------------------------------------------- /plugins/journaltail/journaltail.go: -------------------------------------------------------------------------------- 1 | package journaltail 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | 8 | "flashcat.cloud/catpaw/config" 9 | "flashcat.cloud/catpaw/logger" 10 | "flashcat.cloud/catpaw/pkg/filter" 11 | "flashcat.cloud/catpaw/pkg/safe" 12 | "flashcat.cloud/catpaw/plugins" 13 | "flashcat.cloud/catpaw/types" 14 | ) 15 | 16 | const ( 17 | pluginName string = "journaltail" 18 | ) 19 | 20 | type Instance struct { 21 | config.InternalConfig 22 | 23 | TimeSpan string `toml:"time_span"` 24 | Check string `toml:"check"` 25 | FilterInclude []string `toml:"filter_include"` 26 | FilterExclude []string `toml:"filter_exclude"` 27 | 28 | filter filter.Filter 29 | } 30 | 31 | type JournaltailPlugin struct { 32 | config.InternalConfig 33 | Instances []*Instance `toml:"instances"` 34 | } 35 | 36 | func (p *JournaltailPlugin) GetInstances() []plugins.Instance { 37 | ret := make([]plugins.Instance, len(p.Instances)) 38 | for i := 0; i < len(p.Instances); i++ { 39 | ret[i] = p.Instances[i] 40 | } 41 | return ret 42 | } 43 | 44 | func init() { 45 | plugins.Add(pluginName, func() plugins.Plugin { 46 | return &JournaltailPlugin{} 47 | }) 48 | } 49 | 50 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 51 | if ins.TimeSpan == "" { 52 | ins.TimeSpan = "1m" 53 | } 54 | 55 | if ins.filter == nil { 56 | if len(ins.FilterInclude) == 0 && len(ins.FilterExclude) == 0 { 57 | logger.Logger.Error("filter_include and filter_exclude are empty") 58 | return 59 | } 60 | 61 | var err error 62 | ins.filter, err = filter.NewIncludeExcludeFilter(ins.FilterInclude, ins.FilterExclude) 63 | if err != nil { 64 | logger.Logger.Warnf("failed to create filter: %s", err) 65 | return 66 | } 67 | } 68 | 69 | if ins.Check == "" { 70 | logger.Logger.Error("check is empty") 71 | return 72 | } 73 | 74 | // go go go 75 | bin, err := exec.LookPath("journalctl") 76 | if err != nil { 77 | logger.Logger.Error("lookup journalctl fail: ", err) 78 | return 79 | } 80 | 81 | if bin == "" { 82 | logger.Logger.Error("journalctl not found") 83 | return 84 | } 85 | 86 | out, err := exec.Command(bin, "--since", fmt.Sprintf("-%s", ins.TimeSpan), "--no-pager", "--no-tail").Output() 87 | if err != nil { 88 | logger.Logger.Error("exec journalctl fail: ", err) 89 | return 90 | } 91 | 92 | var bs bytes.Buffer 93 | var triggered bool 94 | 95 | bs.WriteString("[MD]") 96 | bs.WriteString(fmt.Sprintf("- time_span: `%s`\n", ins.TimeSpan)) 97 | bs.WriteString(fmt.Sprintf("- filter_include: `%s`\n", ins.FilterInclude)) 98 | bs.WriteString(fmt.Sprintf("- filter_exclude: `%s`\n", ins.FilterExclude)) 99 | bs.WriteString("\n") 100 | bs.WriteString("\n") 101 | bs.WriteString("**matched lines**:\n") 102 | bs.WriteString("\n```") 103 | 104 | for _, line := range bytes.Split(out, []byte("\n")) { 105 | if len(line) == 0 { 106 | continue 107 | } 108 | 109 | if !ins.filter.Match(string(line)) { 110 | continue 111 | } 112 | 113 | triggered = true 114 | bs.Write(line) 115 | bs.Write([]byte("\n")) 116 | } 117 | 118 | bs.WriteString("```") 119 | 120 | e := types.BuildEvent(map[string]string{ 121 | "check": ins.Check, 122 | }).SetTitleRule("$check") 123 | 124 | if !triggered { 125 | q.PushFront(e) 126 | return 127 | } 128 | 129 | e.SetEventStatus(ins.GetDefaultSeverity()) 130 | e.SetDescription(bs.String()) 131 | q.PushFront(e) 132 | } 133 | -------------------------------------------------------------------------------- /plugins/mtime/mtime.go: -------------------------------------------------------------------------------- 1 | package mtime 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "flashcat.cloud/catpaw/config" 11 | "flashcat.cloud/catpaw/logger" 12 | "flashcat.cloud/catpaw/pkg/safe" 13 | "flashcat.cloud/catpaw/plugins" 14 | "flashcat.cloud/catpaw/types" 15 | "github.com/toolkits/pkg/file" 16 | ) 17 | 18 | const ( 19 | pluginName string = "mtime" 20 | ) 21 | 22 | type Instance struct { 23 | config.InternalConfig 24 | 25 | TimeSpan time.Duration `toml:"time_span"` 26 | Directory string `toml:"directory"` 27 | Check string `toml:"check"` 28 | } 29 | 30 | type MTimePlugin struct { 31 | config.InternalConfig 32 | Instances []*Instance `toml:"instances"` 33 | } 34 | 35 | func (p *MTimePlugin) GetInstances() []plugins.Instance { 36 | ret := make([]plugins.Instance, len(p.Instances)) 37 | for i := 0; i < len(p.Instances); i++ { 38 | ret[i] = p.Instances[i] 39 | } 40 | return ret 41 | } 42 | 43 | func init() { 44 | plugins.Add(pluginName, func() plugins.Plugin { 45 | return &MTimePlugin{} 46 | }) 47 | } 48 | 49 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 50 | if ins.TimeSpan == 0 { 51 | ins.TimeSpan = 3 * time.Minute 52 | } 53 | 54 | if ins.Check == "" { 55 | logger.Logger.Error("check is empty") 56 | return 57 | } 58 | 59 | if !file.IsExist(ins.Directory) { 60 | logger.Logger.Warnf("directory %s not exist", ins.Directory) 61 | return 62 | } 63 | 64 | now := time.Now() 65 | files := make(map[string]time.Time) 66 | 67 | if err := filepath.WalkDir(ins.Directory, func(path string, di fs.DirEntry, err error) error { 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if di.IsDir() { 73 | return nil 74 | } 75 | 76 | fileinfo, err := di.Info() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | mtime := fileinfo.ModTime() 82 | if now.Sub(mtime) < ins.TimeSpan { 83 | files[path] = mtime 84 | } 85 | 86 | return nil 87 | }); err != nil { 88 | q.PushFront(ins.buildEvent(ins.Directory, ins.Check, fmt.Sprintf("walk directory %s error: %v", ins.Directory, err)).SetEventStatus(ins.GetDefaultSeverity())) 89 | return 90 | } 91 | 92 | if len(files) == 0 { 93 | q.PushFront(ins.buildEvent(ins.Directory, ins.Check, fmt.Sprintf("files not changed or created in directory %s", ins.Directory))) 94 | return 95 | } 96 | 97 | var body strings.Builder 98 | body.WriteString(head) 99 | 100 | for fp, mtime := range files { 101 | body.WriteString("| ") 102 | body.WriteString(fp) 103 | body.WriteString(" | ") 104 | body.WriteString(mtime.Format("2006-01-02 15:04:05")) 105 | body.WriteString(" |\n") 106 | } 107 | 108 | q.PushFront(ins.buildEvent(ins.Directory, ins.Check, body.String()).SetEventStatus(ins.GetDefaultSeverity())) 109 | } 110 | 111 | func (ins *Instance) buildEvent(dir, check string, desc ...string) *types.Event { 112 | event := types.BuildEvent(map[string]string{"directory": dir, "check": check}).SetTitleRule("$check") 113 | if len(desc) > 0 { 114 | event.SetDescription(desc[0]) 115 | } 116 | return event 117 | } 118 | 119 | var head = `[MD]| File | MTime | 120 | | :--| --: | 121 | ` 122 | -------------------------------------------------------------------------------- /plugins/net/net.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/textproto" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "flashcat.cloud/catpaw/config" 14 | "flashcat.cloud/catpaw/logger" 15 | "flashcat.cloud/catpaw/pkg/safe" 16 | "flashcat.cloud/catpaw/plugins" 17 | "flashcat.cloud/catpaw/types" 18 | "github.com/toolkits/pkg/concurrent/semaphore" 19 | ) 20 | 21 | const pluginName = "net" 22 | 23 | type Partial struct { 24 | ID string `toml:"id"` 25 | Concurrency int `toml:"concurrency"` 26 | Timeout config.Duration `toml:"timeout"` 27 | ReadTimeout config.Duration `toml:"read_timeout"` 28 | Protocol string `toml:"protocol"` 29 | Send string `toml:"send"` 30 | Expect string `toml:"expect"` 31 | } 32 | 33 | type Instance struct { 34 | config.InternalConfig 35 | Partial string `toml:"partial"` 36 | 37 | Targets []string `toml:"targets"` 38 | Concurrency int `toml:"concurrency"` 39 | Timeout config.Duration `toml:"timeout"` 40 | ReadTimeout config.Duration `toml:"read_timeout"` 41 | Protocol string `toml:"protocol"` 42 | Send string `toml:"send"` 43 | Expect string `toml:"expect"` 44 | } 45 | 46 | type NETPlugin struct { 47 | config.InternalConfig 48 | Partials []Partial `toml:"partials"` 49 | Instances []*Instance `toml:"instances"` 50 | } 51 | 52 | func (p *NETPlugin) ApplyPartials() error { 53 | for i := 0; i < len(p.Instances); i++ { 54 | id := p.Instances[i].Partial 55 | if id != "" { 56 | for _, partial := range p.Partials { 57 | if partial.ID == id { 58 | // use partial config as default 59 | if p.Instances[i].Concurrency == 0 { 60 | p.Instances[i].Concurrency = partial.Concurrency 61 | } 62 | 63 | if p.Instances[i].Timeout == 0 { 64 | p.Instances[i].Timeout = partial.Timeout 65 | } 66 | 67 | if p.Instances[i].ReadTimeout == 0 { 68 | p.Instances[i].ReadTimeout = partial.ReadTimeout 69 | } 70 | 71 | if p.Instances[i].Protocol == "" { 72 | p.Instances[i].Protocol = partial.Protocol 73 | } 74 | 75 | if p.Instances[i].Send == "" { 76 | p.Instances[i].Send = partial.Send 77 | } 78 | 79 | if p.Instances[i].Expect == "" { 80 | p.Instances[i].Expect = partial.Expect 81 | } 82 | 83 | break 84 | } 85 | } 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | func init() { 92 | plugins.Add(pluginName, func() plugins.Plugin { 93 | return &NETPlugin{} 94 | }) 95 | } 96 | 97 | func (p *NETPlugin) GetInstances() []plugins.Instance { 98 | ret := make([]plugins.Instance, len(p.Instances)) 99 | for i := 0; i < len(p.Instances); i++ { 100 | ret[i] = p.Instances[i] 101 | } 102 | return ret 103 | } 104 | 105 | func (ins *Instance) Init() error { 106 | if ins.Concurrency == 0 { 107 | ins.Concurrency = 10 108 | } 109 | 110 | if ins.Timeout == 0 { 111 | ins.Timeout = config.Duration(time.Second) 112 | } 113 | 114 | if ins.ReadTimeout == 0 { 115 | ins.ReadTimeout = config.Duration(time.Second) 116 | } 117 | 118 | if ins.Protocol == "" { 119 | ins.Protocol = "tcp" 120 | } 121 | 122 | if ins.Protocol != "tcp" && ins.Protocol != "udp" { 123 | return errors.New("bad protocol, only tcp and udp are supported") 124 | } 125 | 126 | if ins.Protocol == "udp" && ins.Send == "" { 127 | return errors.New("send string cannot be empty when protocol is udp") 128 | } 129 | 130 | if ins.Protocol == "udp" && ins.Expect == "" { 131 | return errors.New("expected string cannot be empty when protocol is udp") 132 | } 133 | 134 | for i := 0; i < len(ins.Targets); i++ { 135 | target := ins.Targets[i] 136 | 137 | host, port, err := net.SplitHostPort(target) 138 | if err != nil { 139 | return fmt.Errorf("failed to split host port, target: %s, error: %v", target, err) 140 | } 141 | 142 | if host == "" { 143 | ins.Targets[i] = "localhost:" + port 144 | } 145 | 146 | if port == "" { 147 | return errors.New("bad port, target: " + target) 148 | } 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 155 | logger.Logger.Debug("net gather, targets: ", ins.Targets) 156 | 157 | if len(ins.Targets) == 0 { 158 | return 159 | } 160 | 161 | if !ins.GetInitialized() { 162 | if err := ins.Init(); err != nil { 163 | logger.Logger.Errorf("failed to init net plugin instance: %v", err) 164 | return 165 | } else { 166 | ins.SetInitialized() 167 | } 168 | } 169 | 170 | wg := new(sync.WaitGroup) 171 | se := semaphore.NewSemaphore(ins.Concurrency) 172 | for _, target := range ins.Targets { 173 | wg.Add(1) 174 | se.Acquire() 175 | go func(target string) { 176 | defer func() { 177 | se.Release() 178 | wg.Done() 179 | }() 180 | ins.gather(q, target) 181 | }(target) 182 | } 183 | wg.Wait() 184 | } 185 | 186 | func (ins *Instance) gather(q *safe.Queue[*types.Event], target string) { 187 | logger.Logger.Debug("net target: ", target) 188 | 189 | labels := map[string]string{ 190 | "target": target, 191 | "protocol": ins.Protocol, 192 | } 193 | 194 | switch ins.Protocol { 195 | case "tcp": 196 | ins.TCPGather(target, labels, q) 197 | case "udp": 198 | ins.UDPGather(target, labels, q) 199 | } 200 | } 201 | 202 | func (ins *Instance) TCPGather(address string, labels map[string]string, q *safe.Queue[*types.Event]) { 203 | event := types.BuildEvent(map[string]string{ 204 | "check": "tcp check", 205 | }, labels).SetTitleRule("$check").SetDescription(ins.buildDesc(address, "everything is ok")) 206 | 207 | conn, err := net.DialTimeout("tcp", address, time.Duration(ins.Timeout)) 208 | if err != nil { 209 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("connection error: %v", err)))) 210 | logger.Logger.Errorw("failed to send tcp request", "error", err, "plugin", pluginName, "target", address) 211 | return 212 | } 213 | 214 | defer conn.Close() 215 | 216 | // check expect string 217 | if ins.Send == "" { 218 | // no need check send and expect 219 | q.PushFront(event) 220 | return 221 | } 222 | 223 | msg := []byte(ins.Send) 224 | if _, err = conn.Write(msg); err != nil { 225 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("failed to send message: %s, error: %v", ins.Send, err)))) 226 | return 227 | } 228 | 229 | // Read string if needed 230 | if ins.Expect != "" { 231 | // Set read timeout 232 | if err := conn.SetReadDeadline(time.Now().Add(time.Duration(ins.ReadTimeout))); err != nil { 233 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("failed to set read deadline, error: %v", err)))) 234 | return 235 | } 236 | 237 | // Prepare reader 238 | reader := bufio.NewReader(conn) 239 | tp := textproto.NewReader(reader) 240 | // Read 241 | data, err := tp.ReadLine() 242 | if err != nil { 243 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("failed to read response line, error: %v", err)))) 244 | return 245 | } 246 | 247 | if !strings.Contains(data, ins.Expect) { 248 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, fmt.Sprintf("response mismatch. expected: %s, real response: %s", ins.Expect, data)))) 249 | return 250 | } 251 | } 252 | 253 | q.PushFront(event) 254 | } 255 | 256 | func (ins *Instance) UDPGather(address string, labels map[string]string, q *safe.Queue[*types.Event]) { 257 | event := types.BuildEvent(map[string]string{ 258 | "check": "udp check", 259 | }, labels).SetTitleRule("$check").SetDescription(ins.buildDesc(address, "everything is ok")) 260 | 261 | udpAddr, err := net.ResolveUDPAddr("udp", address) 262 | if err != nil { 263 | message := fmt.Sprintf("resolve udp address(%s) error: %v", address, err) 264 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message))) 265 | logger.Logger.Error(message) 266 | return 267 | } 268 | 269 | conn, err := net.DialUDP("udp", nil, udpAddr) 270 | if err != nil { 271 | message := fmt.Sprintf("dial udp address(%s) error: %v", address, err) 272 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message))) 273 | logger.Logger.Error(message) 274 | return 275 | } 276 | 277 | defer conn.Close() 278 | 279 | if _, err = conn.Write([]byte(ins.Send)); err != nil { 280 | message := fmt.Sprintf("write string(%s) to udp address(%s) error: %v", ins.Send, address, err) 281 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message))) 282 | logger.Logger.Error(message) 283 | return 284 | } 285 | 286 | if err = conn.SetReadDeadline(time.Now().Add(time.Duration(ins.ReadTimeout))); err != nil { 287 | message := fmt.Sprintf("set connection deadline to udp address(%s) error: %v", address, err) 288 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message))) 289 | logger.Logger.Error(message) 290 | return 291 | } 292 | 293 | // Read 294 | buf := make([]byte, 1024) 295 | if _, _, err = conn.ReadFromUDP(buf); err != nil { 296 | message := fmt.Sprintf("read from udp address(%s) error: %v", address, err) 297 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message))) 298 | logger.Logger.Error(message) 299 | return 300 | } 301 | 302 | if !strings.Contains(string(buf), ins.Expect) { 303 | message := fmt.Sprintf("response mismatch. expect: %s, real: %s", ins.Expect, string(buf)) 304 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(address, message))) 305 | logger.Logger.Error(message) 306 | return 307 | } 308 | 309 | q.PushFront(event) 310 | } 311 | 312 | func (ins *Instance) buildDesc(target, message string) string { 313 | return `[MD] 314 | - **target**: ` + target + ` 315 | - **protocol**: ` + ins.Protocol + ` 316 | - **config.send**: ` + ins.Send + ` 317 | - **config.expect**:` + ins.Expect + ` 318 | 319 | 320 | **message**: 321 | 322 | ` + "```" + ` 323 | ` + message + ` 324 | ` + "```" + ` 325 | ` 326 | } 327 | -------------------------------------------------------------------------------- /plugins/ping/ping.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "runtime" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "flashcat.cloud/catpaw/config" 12 | "flashcat.cloud/catpaw/logger" 13 | "flashcat.cloud/catpaw/pkg/safe" 14 | "flashcat.cloud/catpaw/plugins" 15 | "flashcat.cloud/catpaw/types" 16 | ping "github.com/prometheus-community/pro-bing" 17 | ) 18 | 19 | const ( 20 | pluginName = "ping" 21 | defaultPingDataBytesSize = 56 22 | ) 23 | 24 | type Partial struct { 25 | ID string `toml:"id"` 26 | Concurrency int `toml:"concurrency"` 27 | Count int `toml:"count"` // ping -c 28 | PingInterval float64 `toml:"ping_interval"` // ping -i 29 | Timeout float64 `toml:"timeout"` // ping -W 30 | Interface string `toml:"interface"` // ping -I/-S 31 | IPv6 *bool `toml:"ipv6"` // Whether to resolve addresses using ipv6 or not. 32 | Size *int `toml:"size"` // Packet size 33 | AlertIfPacketLossPercentGe float64 `toml:"alert_if_packet_loss_percent_ge"` 34 | } 35 | 36 | type Instance struct { 37 | config.InternalConfig 38 | Partial string `toml:"partial"` 39 | 40 | Targets []string `toml:"targets"` 41 | Concurrency int `toml:"concurrency"` 42 | Count int `toml:"count"` // ping -c 43 | PingInterval float64 `toml:"ping_interval"` // ping -i 44 | Timeout float64 `toml:"timeout"` // ping -W 45 | Interface string `toml:"interface"` // ping -I/-S 46 | IPv6 *bool `toml:"ipv6"` // Whether to resolve addresses using ipv6 or not. 47 | Size *int `toml:"size"` // Packet size 48 | AlertIfPacketLossPercentGe float64 `toml:"alert_if_packet_loss_percent_ge"` 49 | 50 | calcInterval time.Duration 51 | calcTimeout time.Duration 52 | sourceAddress string 53 | } 54 | 55 | type PingPlugin struct { 56 | config.InternalConfig 57 | Partials []Partial `toml:"partials"` 58 | Instances []*Instance `toml:"instances"` 59 | } 60 | 61 | func (p *PingPlugin) ApplyPartials() error { 62 | for i := 0; i < len(p.Instances); i++ { 63 | id := p.Instances[i].Partial 64 | if id != "" { 65 | for _, partial := range p.Partials { 66 | if partial.ID == id { 67 | // use partial config as default 68 | if p.Instances[i].Concurrency == 0 { 69 | p.Instances[i].Concurrency = partial.Concurrency 70 | } 71 | 72 | if p.Instances[i].Count == 0 { 73 | p.Instances[i].Count = partial.Count 74 | } 75 | 76 | if p.Instances[i].PingInterval == 0 { 77 | p.Instances[i].PingInterval = partial.PingInterval 78 | } 79 | 80 | if p.Instances[i].Timeout == 0 { 81 | p.Instances[i].Timeout = partial.Timeout 82 | } 83 | 84 | if p.Instances[i].Interface == "" { 85 | p.Instances[i].Interface = partial.Interface 86 | } 87 | 88 | if p.Instances[i].IPv6 == nil { 89 | p.Instances[i].IPv6 = partial.IPv6 90 | } 91 | 92 | if p.Instances[i].Size == nil { 93 | p.Instances[i].Size = partial.Size 94 | } 95 | 96 | if p.Instances[i].AlertIfPacketLossPercentGe == 0 { 97 | p.Instances[i].AlertIfPacketLossPercentGe = partial.AlertIfPacketLossPercentGe 98 | } 99 | 100 | break 101 | } 102 | } 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | func init() { 109 | plugins.Add(pluginName, func() plugins.Plugin { 110 | return &PingPlugin{} 111 | }) 112 | } 113 | 114 | func (p *PingPlugin) GetInstances() []plugins.Instance { 115 | ret := make([]plugins.Instance, len(p.Instances)) 116 | for i := 0; i < len(p.Instances); i++ { 117 | ret[i] = p.Instances[i] 118 | } 119 | return ret 120 | } 121 | 122 | func (ins *Instance) Init() error { 123 | if ins.Concurrency == 0 { 124 | ins.Concurrency = 10 125 | } 126 | 127 | if ins.Count < 1 { 128 | ins.Count = 5 129 | } 130 | 131 | if ins.PingInterval < 0.2 { 132 | ins.calcInterval = time.Duration(0.2 * float64(time.Second)) 133 | } else { 134 | ins.calcInterval = time.Duration(ins.PingInterval * float64(time.Second)) 135 | } 136 | 137 | if ins.Timeout == 0 { 138 | ins.calcTimeout = time.Duration(3) * time.Second 139 | } else { 140 | ins.calcTimeout = time.Duration(ins.Timeout * float64(time.Second)) 141 | } 142 | 143 | if ins.Interface != "" { 144 | if addr := net.ParseIP(ins.Interface); addr != nil { 145 | ins.sourceAddress = ins.Interface 146 | } else { 147 | i, err := net.InterfaceByName(ins.Interface) 148 | if err != nil { 149 | return fmt.Errorf("failed to get interface: %v", err) 150 | } 151 | 152 | addrs, err := i.Addrs() 153 | if err != nil { 154 | return fmt.Errorf("failed to get the address of interface: %v", err) 155 | } 156 | 157 | ins.sourceAddress = addrs[0].(*net.IPNet).IP.String() 158 | } 159 | } 160 | 161 | return nil 162 | } 163 | 164 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 165 | logger.Logger.Debug("ping... targets: ", ins.Targets) 166 | 167 | if len(ins.Targets) == 0 { 168 | return 169 | } 170 | 171 | if !ins.GetInitialized() { 172 | if err := ins.Init(); err != nil { 173 | logger.Logger.Errorf("failed to init ping plugin instance: %v", err) 174 | return 175 | } else { 176 | ins.SetInitialized() 177 | } 178 | } 179 | 180 | wg := new(sync.WaitGroup) 181 | se := make(chan struct{}, ins.Concurrency) 182 | for _, target := range ins.Targets { 183 | wg.Add(1) 184 | se <- struct{}{} 185 | go func(target string) { 186 | defer func() { 187 | <-se 188 | wg.Done() 189 | }() 190 | ins.gather(q, target) 191 | }(target) 192 | } 193 | wg.Wait() 194 | close(se) 195 | } 196 | 197 | func (ins *Instance) gather(q *safe.Queue[*types.Event], target string) { 198 | logger.Logger.Debug("ping target: ", target) 199 | 200 | labels := map[string]string{ 201 | "target": target, 202 | } 203 | 204 | event := types.BuildEvent(map[string]string{ 205 | "check": "ping check", 206 | }, labels).SetTitleRule("$check").SetDescription(ins.buildDesc(target, "everything is ok")) 207 | 208 | stats, err := ins.ping(target) 209 | if err != nil { 210 | message := fmt.Sprintf("ping %s failed: %v", target, err) 211 | logger.Logger.Error(message) 212 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message))) 213 | return 214 | } 215 | 216 | if stats.PacketsSent == 0 { 217 | message := fmt.Sprintf("no packets sent to %s", target) 218 | logger.Logger.Error(message) 219 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message))) 220 | return 221 | } 222 | 223 | if stats.PacketsRecv == 0 { 224 | message := fmt.Sprintf("no packets received to %s", target) 225 | logger.Logger.Error(message) 226 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message))) 227 | return 228 | } 229 | 230 | if stats.PacketLoss >= float64(ins.AlertIfPacketLossPercentGe) { 231 | message := fmt.Sprintf("packet loss is %f%%", stats.PacketLoss) 232 | logger.Logger.Error(message) 233 | q.PushFront(event.SetEventStatus(ins.GetDefaultSeverity()).SetDescription(ins.buildDesc(target, message))) 234 | return 235 | } 236 | 237 | q.PushFront(event) 238 | } 239 | 240 | type pingStats struct { 241 | ping.Statistics 242 | ttl int 243 | } 244 | 245 | func (ins *Instance) ping(destination string) (*pingStats, error) { 246 | ps := &pingStats{} 247 | 248 | pinger, err := ping.NewPinger(destination) 249 | if err != nil { 250 | return nil, fmt.Errorf("failed to create new pinger: %w", err) 251 | } 252 | 253 | pinger.SetPrivileged(true) 254 | 255 | if ins.IPv6 != nil && *ins.IPv6 { 256 | pinger.SetNetwork("ip6") 257 | } 258 | 259 | pinger.Size = defaultPingDataBytesSize 260 | if ins.Size != nil { 261 | pinger.Size = *ins.Size 262 | } 263 | 264 | pinger.Source = ins.sourceAddress 265 | pinger.Interval = ins.calcInterval 266 | pinger.Timeout = ins.calcTimeout 267 | 268 | // Get Time to live (TTL) of first response, matching original implementation 269 | once := &sync.Once{} 270 | pinger.OnRecv = func(pkt *ping.Packet) { 271 | once.Do(func() { 272 | ps.ttl = pkt.TTL 273 | }) 274 | } 275 | 276 | pinger.Count = ins.Count 277 | err = pinger.Run() 278 | if err != nil { 279 | if strings.Contains(err.Error(), "operation not permitted") { 280 | if runtime.GOOS == "linux" { 281 | return nil, fmt.Errorf("permission changes required, enable CAP_NET_RAW capabilities (refer to the ping plugin's README.md for more info)") 282 | } 283 | 284 | return nil, fmt.Errorf("permission changes required, refer to the ping plugin's README.md for more info") 285 | } 286 | return nil, fmt.Errorf("%w", err) 287 | } 288 | 289 | ps.Statistics = *pinger.Statistics() 290 | 291 | return ps, nil 292 | } 293 | 294 | func (ins *Instance) buildDesc(target, message string) string { 295 | return `[MD] 296 | - target: ` + target + ` 297 | - alert_if_packet_loss_percent_ge: ` + fmt.Sprint(ins.AlertIfPacketLossPercentGe) + ` 298 | 299 | 300 | **message**: 301 | 302 | ` + "```" + ` 303 | ` + message + ` 304 | ` + "```" + ` 305 | ` 306 | } 307 | -------------------------------------------------------------------------------- /plugins/plugins.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "flashcat.cloud/catpaw/config" 5 | "flashcat.cloud/catpaw/pkg/safe" 6 | "flashcat.cloud/catpaw/types" 7 | ) 8 | 9 | type Instance interface { 10 | GetLabels() map[string]string 11 | GetInterval() config.Duration 12 | GetAlerting() config.Alerting 13 | InitInternalConfig() error 14 | } 15 | 16 | type Plugin interface { 17 | GetLabels() map[string]string 18 | GetInterval() config.Duration 19 | // GetAlerting() config.Alerting 20 | // InitInternalConfig() error 21 | } 22 | 23 | type IApplyPartials interface { 24 | ApplyPartials() error 25 | } 26 | 27 | type Gatherer interface { 28 | Gather(*safe.Queue[*types.Event]) 29 | } 30 | 31 | func MayApplyPartials(p interface{}) error { 32 | if ap, ok := p.(IApplyPartials); ok { 33 | return ap.ApplyPartials() 34 | } 35 | return nil 36 | } 37 | 38 | type Dropper interface { 39 | Drop() 40 | } 41 | 42 | type InstancesGetter interface { 43 | GetInstances() []Instance 44 | } 45 | 46 | func MayGather(t interface{}, q *safe.Queue[*types.Event]) { 47 | if gather, ok := t.(Gatherer); ok { 48 | gather.Gather(q) 49 | } 50 | } 51 | 52 | func MayDrop(t interface{}) { 53 | if dropper, ok := t.(Dropper); ok { 54 | dropper.Drop() 55 | } 56 | } 57 | 58 | func MayGetInstances(t interface{}) []Instance { 59 | if instancesGetter, ok := t.(InstancesGetter); ok { 60 | return instancesGetter.GetInstances() 61 | } 62 | return nil 63 | } 64 | 65 | type Creator func() Plugin 66 | 67 | var PluginCreators = map[string]Creator{} 68 | 69 | func Add(name string, creator Creator) { 70 | PluginCreators[name] = creator 71 | } 72 | -------------------------------------------------------------------------------- /plugins/procnum/native_finder.go: -------------------------------------------------------------------------------- 1 | package procnum 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/shirou/gopsutil/v3/process" 10 | ) 11 | 12 | type Filter func(p *process.Process) bool 13 | 14 | // NativeFinder uses gopsutil to find processes 15 | type NativeFinder struct { 16 | } 17 | 18 | func NewNativeFinder() PIDFinder { 19 | return &NativeFinder{} 20 | } 21 | 22 | // Uid will return all pids for the given user 23 | func (pg *NativeFinder) UID(user string) ([]PID, error) { 24 | var dst []PID 25 | procs, err := process.Processes() 26 | if err != nil { 27 | return dst, err 28 | } 29 | for _, p := range procs { 30 | username, err := p.Username() 31 | if err != nil { 32 | // skip, this can happen if we don't have permissions or 33 | // the pid no longer exists 34 | continue 35 | } 36 | if username == user { 37 | dst = append(dst, PID(p.Pid)) 38 | } 39 | } 40 | return dst, nil 41 | } 42 | 43 | // PidFile returns the pid from the pid file given. 44 | func (pg *NativeFinder) PidFile(path string) ([]PID, error) { 45 | var pids []PID 46 | pidString, err := os.ReadFile(path) 47 | if err != nil { 48 | return pids, fmt.Errorf("failed to read pidfile '%s'. Error: '%s'", 49 | path, err) 50 | } 51 | pid, err := strconv.ParseInt(strings.TrimSpace(string(pidString)), 10, 32) 52 | if err != nil { 53 | return pids, err 54 | } 55 | pids = append(pids, PID(pid)) 56 | return pids, nil 57 | } 58 | 59 | // FullPattern matches on the command line when the process was executed 60 | func (pg *NativeFinder) FullPattern(pattern string, filters ...Filter) ([]PID, error) { 61 | var pids []PID 62 | 63 | procs, err := pg.FastProcessList() 64 | if err != nil { 65 | return pids, err 66 | } 67 | PROCS: 68 | for _, p := range procs { 69 | for _, filter := range filters { 70 | if !filter(p) { 71 | continue PROCS 72 | } 73 | } 74 | cmd, err := p.Cmdline() 75 | if err != nil { 76 | // skip, this can be caused by the pid no longer existing 77 | // or you having no permissions to access it 78 | continue 79 | } 80 | if strings.Contains(cmd, pattern) { 81 | pids = append(pids, PID(p.Pid)) 82 | } 83 | } 84 | return pids, err 85 | 86 | } 87 | 88 | func (pg *NativeFinder) FastProcessList() ([]*process.Process, error) { 89 | pids, err := process.Pids() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | result := make([]*process.Process, len(pids)) 95 | for i, pid := range pids { 96 | result[i] = &process.Process{Pid: pid} 97 | } 98 | return result, nil 99 | } 100 | -------------------------------------------------------------------------------- /plugins/procnum/native_finder_notwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package procnum 5 | 6 | import ( 7 | "strings" 8 | ) 9 | 10 | // Pattern matches on the process name 11 | func (pg *NativeFinder) Pattern(pattern string, filters ...Filter) ([]PID, error) { 12 | var pids []PID 13 | procs, err := pg.FastProcessList() 14 | if err != nil { 15 | return pids, err 16 | } 17 | PROCS: 18 | for _, p := range procs { 19 | for _, filter := range filters { 20 | if !filter(p) { 21 | continue PROCS 22 | } 23 | } 24 | name, err := p.Exe() 25 | if err != nil { 26 | // skip, this can be caused by the pid no longer existing 27 | // or you having no permissions to access it 28 | continue 29 | } 30 | if strings.Contains(name, pattern) { 31 | pids = append(pids, PID(p.Pid)) 32 | } 33 | } 34 | return pids, err 35 | } 36 | -------------------------------------------------------------------------------- /plugins/procnum/native_finder_windows.go: -------------------------------------------------------------------------------- 1 | package procnum 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Pattern matches on the process name 8 | func (pg *NativeFinder) Pattern(pattern string, filters ...Filter) ([]PID, error) { 9 | var pids []PID 10 | procs, err := pg.FastProcessList() 11 | if err != nil { 12 | return pids, err 13 | } 14 | PROCS: 15 | for _, p := range procs { 16 | for _, filter := range filters { 17 | if !filter(p) { 18 | continue PROCS 19 | } 20 | } 21 | name, err := p.Name() 22 | if err != nil { 23 | // skip, this can be caused by the pid no longer existing 24 | // or you having no permissions to access it 25 | continue 26 | } 27 | if strings.Contains(name, pattern) { 28 | pids = append(pids, PID(p.Pid)) 29 | } 30 | } 31 | return pids, err 32 | } 33 | -------------------------------------------------------------------------------- /plugins/procnum/process.go: -------------------------------------------------------------------------------- 1 | package procnum 2 | 3 | type PID int32 4 | 5 | type PIDFinder interface { 6 | PidFile(path string) ([]PID, error) 7 | Pattern(pattern string, filters ...Filter) ([]PID, error) 8 | UID(user string) ([]PID, error) 9 | FullPattern(path string, filters ...Filter) ([]PID, error) 10 | } 11 | -------------------------------------------------------------------------------- /plugins/procnum/procnum.go: -------------------------------------------------------------------------------- 1 | package procnum 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "flashcat.cloud/catpaw/config" 8 | "flashcat.cloud/catpaw/logger" 9 | "flashcat.cloud/catpaw/pkg/safe" 10 | "flashcat.cloud/catpaw/plugins" 11 | "flashcat.cloud/catpaw/types" 12 | ) 13 | 14 | const ( 15 | pluginName string = "procnum" 16 | ) 17 | 18 | type Instance struct { 19 | config.InternalConfig 20 | 21 | SearchExecSubstring string `toml:"search_exec_substring"` 22 | SearchCmdlineSubstring string `toml:"search_cmdline_substring"` 23 | SearchWinService string `toml:"search_win_service"` 24 | 25 | searchString string 26 | 27 | AlertIfNumLt int `toml:"alert_if_num_lt"` 28 | Check string `toml:"check"` 29 | } 30 | 31 | type ProcnumPlugin struct { 32 | config.InternalConfig 33 | Instances []*Instance `toml:"instances"` 34 | } 35 | 36 | func (p *ProcnumPlugin) GetInstances() []plugins.Instance { 37 | ret := make([]plugins.Instance, len(p.Instances)) 38 | for i := 0; i < len(p.Instances); i++ { 39 | ret[i] = p.Instances[i] 40 | } 41 | return ret 42 | } 43 | 44 | func init() { 45 | plugins.Add(pluginName, func() plugins.Plugin { 46 | return &ProcnumPlugin{} 47 | }) 48 | } 49 | 50 | func (ins *Instance) Init() error { 51 | if ins.SearchExecSubstring != "" { 52 | ins.searchString = ins.SearchExecSubstring 53 | } else if ins.SearchCmdlineSubstring != "" { 54 | ins.searchString = ins.SearchCmdlineSubstring 55 | } else if ins.SearchWinService != "" { 56 | ins.searchString = ins.SearchWinService 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 63 | if !ins.GetInitialized() { 64 | if err := ins.Init(); err != nil { 65 | logger.Logger.Errorf("failed to init procnum plugin instance: %v", err) 66 | return 67 | } else { 68 | ins.SetInitialized() 69 | } 70 | } 71 | 72 | if ins.searchString == "" { 73 | return 74 | } 75 | 76 | if ins.Check == "" { 77 | logger.Logger.Error("check is empty") 78 | return 79 | } 80 | 81 | var ( 82 | pids []PID 83 | err error 84 | ) 85 | 86 | pg := NewNativeFinder() 87 | if ins.SearchExecSubstring != "" { 88 | pids, err = pg.Pattern(ins.SearchExecSubstring) 89 | } else if ins.SearchCmdlineSubstring != "" { 90 | pids, err = pg.FullPattern(ins.SearchCmdlineSubstring) 91 | } else if ins.SearchWinService != "" { 92 | pids, err = ins.winServicePIDs() 93 | } else { 94 | logger.Logger.Error("Oops... search string not found") 95 | return 96 | } 97 | 98 | if err != nil { 99 | q.PushFront(ins.buildEvent("Occur error: " + err.Error()).SetEventStatus(ins.GetDefaultSeverity())) 100 | return 101 | } 102 | 103 | logger.Logger.Debugf("search string: %s, pids: %v", ins.searchString, pids) 104 | 105 | if len(pids) < ins.AlertIfNumLt { 106 | s := fmt.Sprintf("The number of process is less than expected. real: %d, expected: %d", len(pids), ins.AlertIfNumLt) 107 | q.PushFront(ins.buildEvent("[MD]", s, ` 108 | - search_exec_substring: `+ins.SearchExecSubstring+` 109 | - search_cmdline_substring: `+ins.SearchCmdlineSubstring+` 110 | - search_win_service: `+ins.SearchWinService+` 111 | `).SetEventStatus(ins.GetDefaultSeverity())) 112 | return 113 | } 114 | 115 | q.PushFront(ins.buildEvent()) 116 | } 117 | 118 | func (ins *Instance) buildEvent(desc ...string) *types.Event { 119 | event := types.BuildEvent(map[string]string{"check": ins.Check}).SetTitleRule("$check") 120 | if len(desc) > 0 { 121 | event.SetDescription(strings.Join(desc, "\n")) 122 | } 123 | return event 124 | } 125 | 126 | func (ins *Instance) winServicePIDs() ([]PID, error) { 127 | var pids []PID 128 | 129 | pid, err := queryPidWithWinServiceName(ins.SearchWinService) 130 | if err != nil { 131 | return pids, err 132 | } 133 | 134 | pids = append(pids, PID(pid)) 135 | 136 | return pids, nil 137 | } 138 | -------------------------------------------------------------------------------- /plugins/procnum/win_service_notwindows.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package procnum 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | func queryPidWithWinServiceName(_ string) (uint32, error) { 11 | return 0, fmt.Errorf("os not support win_service option") 12 | } 13 | -------------------------------------------------------------------------------- /plugins/procnum/win_service_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package procnum 5 | 6 | import ( 7 | "unsafe" 8 | 9 | "golang.org/x/sys/windows" 10 | "golang.org/x/sys/windows/svc/mgr" 11 | ) 12 | 13 | func getService(name string) (*mgr.Service, error) { 14 | m, err := mgr.Connect() 15 | if err != nil { 16 | return nil, err 17 | } 18 | defer m.Disconnect() 19 | 20 | srv, err := m.OpenService(name) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return srv, nil 26 | } 27 | 28 | func queryPidWithWinServiceName(winServiceName string) (uint32, error) { 29 | srv, err := getService(winServiceName) 30 | if err != nil { 31 | return 0, err 32 | } 33 | 34 | var p *windows.SERVICE_STATUS_PROCESS 35 | var bytesNeeded uint32 36 | var buf []byte 37 | 38 | if err := windows.QueryServiceStatusEx(srv.Handle, windows.SC_STATUS_PROCESS_INFO, nil, 0, &bytesNeeded); err != windows.ERROR_INSUFFICIENT_BUFFER { 39 | return 0, err 40 | } 41 | 42 | buf = make([]byte, bytesNeeded) 43 | p = (*windows.SERVICE_STATUS_PROCESS)(unsafe.Pointer(&buf[0])) 44 | if err := windows.QueryServiceStatusEx(srv.Handle, windows.SC_STATUS_PROCESS_INFO, &buf[0], uint32(len(buf)), &bytesNeeded); err != nil { 45 | return 0, err 46 | } 47 | 48 | return p.ProcessId, nil 49 | } 50 | -------------------------------------------------------------------------------- /plugins/sfilter/sfilter.go: -------------------------------------------------------------------------------- 1 | package sfilter 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | osExec "os/exec" 8 | "runtime" 9 | "time" 10 | 11 | "flashcat.cloud/catpaw/config" 12 | "flashcat.cloud/catpaw/logger" 13 | "flashcat.cloud/catpaw/pkg/cmdx" 14 | "flashcat.cloud/catpaw/pkg/filter" 15 | "flashcat.cloud/catpaw/pkg/safe" 16 | "flashcat.cloud/catpaw/pkg/shell" 17 | "flashcat.cloud/catpaw/plugins" 18 | "flashcat.cloud/catpaw/types" 19 | ) 20 | 21 | const ( 22 | pluginName string = "sfilter" 23 | maxStderrBytes int = 512 24 | ) 25 | 26 | type Instance struct { 27 | config.InternalConfig 28 | 29 | Command string `toml:"command"` 30 | Timeout config.Duration `toml:"timeout"` 31 | Check string `toml:"check"` 32 | FilterInclude []string `toml:"filter_include"` 33 | FilterExclude []string `toml:"filter_exclude"` 34 | 35 | filter filter.Filter 36 | } 37 | 38 | type SFilterPlugin struct { 39 | config.InternalConfig 40 | Instances []*Instance `toml:"instances"` 41 | } 42 | 43 | func (p *SFilterPlugin) GetInstances() []plugins.Instance { 44 | ret := make([]plugins.Instance, len(p.Instances)) 45 | for i := 0; i < len(p.Instances); i++ { 46 | ret[i] = p.Instances[i] 47 | } 48 | return ret 49 | } 50 | 51 | func init() { 52 | plugins.Add(pluginName, func() plugins.Plugin { 53 | return &SFilterPlugin{} 54 | }) 55 | } 56 | 57 | func (ins *Instance) Gather(q *safe.Queue[*types.Event]) { 58 | if len(ins.Command) == 0 { 59 | return 60 | } 61 | 62 | if ins.Check == "" { 63 | logger.Logger.Warnln("configuration check is empty") 64 | return 65 | } 66 | 67 | if ins.Timeout == 0 { 68 | ins.Timeout = config.Duration(10 * time.Second) 69 | } 70 | 71 | if ins.filter == nil { 72 | if len(ins.FilterInclude) == 0 && len(ins.FilterExclude) == 0 { 73 | logger.Logger.Error("filter_include and filter_exclude are empty") 74 | return 75 | } 76 | 77 | var err error 78 | ins.filter, err = filter.NewIncludeExcludeFilter(ins.FilterInclude, ins.FilterExclude) 79 | if err != nil { 80 | logger.Logger.Warnf("failed to create filter: %s", err) 81 | return 82 | } 83 | } 84 | 85 | ins.gather(q, ins.Command) 86 | } 87 | 88 | func (ins *Instance) gather(q *safe.Queue[*types.Event], command string) { 89 | outbuf, errbuf, err := commandRun(command, time.Duration(ins.Timeout)) 90 | if err != nil || len(errbuf) > 0 { 91 | logger.Logger.Errorw("failed to exec command", "command", command, "error", err, "stderr", string(errbuf), "stdout", string(outbuf)) 92 | return 93 | } 94 | 95 | if len(outbuf) == 0 { 96 | logger.Logger.Warnw("exec command output is empty", "command", command) 97 | return 98 | } 99 | 100 | var bs bytes.Buffer 101 | var triggered bool 102 | 103 | bs.WriteString("[MD]") 104 | bs.WriteString(fmt.Sprintf("- filter_include: `%s`\n", ins.FilterInclude)) 105 | bs.WriteString(fmt.Sprintf("- filter_exclude: `%s`\n", ins.FilterExclude)) 106 | bs.WriteString("\n") 107 | bs.WriteString("\n") 108 | bs.WriteString("**matched lines**:\n") 109 | bs.WriteString("\n```") 110 | 111 | for _, line := range bytes.Split(outbuf, []byte("\n")) { 112 | if len(line) == 0 { 113 | continue 114 | } 115 | 116 | if !ins.filter.Match(string(line)) { 117 | continue 118 | } 119 | 120 | triggered = true 121 | bs.Write(line) 122 | bs.Write([]byte("\n")) 123 | } 124 | 125 | bs.WriteString("```") 126 | 127 | if !triggered { 128 | q.PushFront(types.BuildEvent(map[string]string{"check": ins.Check}).SetTitleRule("$check").SetDescription("everything is ok")) 129 | } else { 130 | q.PushFront(types.BuildEvent(map[string]string{"check": ins.Check}).SetTitleRule("$check").SetDescription(bs.String()).SetEventStatus(ins.GetDefaultSeverity())) 131 | } 132 | } 133 | 134 | func commandRun(command string, timeout time.Duration) ([]byte, []byte, error) { 135 | splitCmd, err := shell.QuoteSplit(command) 136 | if err != nil || len(splitCmd) == 0 { 137 | return nil, nil, fmt.Errorf("exec: unable to parse command, %s", err) 138 | } 139 | 140 | cmd := osExec.Command(splitCmd[0], splitCmd[1:]...) 141 | 142 | var ( 143 | out bytes.Buffer 144 | stderr bytes.Buffer 145 | ) 146 | cmd.Stdout = &out 147 | cmd.Stderr = &stderr 148 | 149 | runError, runTimeout := cmdx.RunTimeout(cmd, timeout) 150 | if runTimeout { 151 | return nil, nil, fmt.Errorf("exec %s timeout", command) 152 | } 153 | 154 | out = removeWindowsCarriageReturns(out) 155 | if stderr.Len() > 0 { 156 | stderr = removeWindowsCarriageReturns(stderr) 157 | stderr = truncate(stderr) 158 | } 159 | 160 | return out.Bytes(), stderr.Bytes(), runError 161 | } 162 | 163 | func truncate(buf bytes.Buffer) bytes.Buffer { 164 | // Limit the number of bytes. 165 | didTruncate := false 166 | if buf.Len() > maxStderrBytes { 167 | buf.Truncate(maxStderrBytes) 168 | didTruncate = true 169 | } 170 | if i := bytes.IndexByte(buf.Bytes(), '\n'); i > 0 { 171 | // Only show truncation if the newline wasn't the last character. 172 | if i < buf.Len()-1 { 173 | didTruncate = true 174 | } 175 | buf.Truncate(i) 176 | } 177 | if didTruncate { 178 | //nolint:errcheck,revive // Will always return nil or panic 179 | buf.WriteString("...") 180 | } 181 | return buf 182 | } 183 | 184 | // removeWindowsCarriageReturns removes all carriage returns from the input if the 185 | // OS is Windows. It does not return any errors. 186 | func removeWindowsCarriageReturns(b bytes.Buffer) bytes.Buffer { 187 | if runtime.GOOS == "windows" { 188 | var buf bytes.Buffer 189 | for { 190 | byt, err := b.ReadBytes(0x0D) 191 | byt = bytes.TrimRight(byt, "\x0d") 192 | if len(byt) > 0 { 193 | _, _ = buf.Write(byt) 194 | } 195 | if err == io.EOF { 196 | return buf 197 | } 198 | } 199 | } 200 | return b 201 | } 202 | -------------------------------------------------------------------------------- /scripts/demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # use plugin exec 3 | echo '[ 4 | { 5 | "event_status": "Warning", 6 | "labels": { 7 | "check": "script demo" 8 | }, 9 | "title_rule": "$check", 10 | "description": "this is description, support markdown" 11 | } 12 | ]' -------------------------------------------------------------------------------- /scripts/df.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # use plugin sfilter 3 | 4 | output=`df -hT` 5 | count=`echo "$output" | grep -c '100%'` 6 | if [ $count -gt 0 ]; then 7 | echo "$output" 8 | fi -------------------------------------------------------------------------------- /scripts/greplog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # use plugin exec 3 | # exec.toml example: 4 | # [[instances]] 5 | # commands = [ 6 | # "/opt/catpaw/scripts/greplog.sh 'Out of memory'" 7 | # ] 8 | # 9 | # deprecated, use plugin journaltail instead 10 | 11 | if [ "$1" ]; then 12 | keyword=$1 13 | else 14 | keyword="Out of memory" 15 | fi 16 | 17 | output=$(journalctl -S-3m | grep "${keyword}") 18 | status="Ok" 19 | if [ -n "$output" ]; then 20 | status="Warning" 21 | fi 22 | 23 | echo '[ 24 | { 25 | "event_status": "'${status}'", 26 | "labels": { 27 | "check": "grep keyword: '${keyword}'" 28 | }, 29 | "title_rule": "$check", 30 | "description": "'${output}'" 31 | } 32 | ]' 33 | -------------------------------------------------------------------------------- /scripts/ulimit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # use plugin exec 3 | 4 | if [ "$1" ]; then 5 | threshhold=$1 6 | else 7 | threshhold=2048 8 | fi 9 | 10 | count=$(ulimit -n) 11 | 12 | status="Ok" 13 | if [ $count -lt $threshhold ]; then 14 | status="Warning" 15 | fi 16 | 17 | echo '[ 18 | { 19 | "event_status": "'${status}'", 20 | "labels": { 21 | "check": "ulimit check" 22 | }, 23 | "title_rule": "$check", 24 | "description": "ulimit -n: '${count}', too low, should be greater than '${threshhold}'" 25 | } 26 | ]' -------------------------------------------------------------------------------- /types/event.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | EventStatusCritical = "Critical" 5 | EventStatusWarning = "Warning" 6 | EventStatusInfo = "Info" 7 | EventStatusOk = "Ok" 8 | ) 9 | 10 | type Event struct { 11 | EventTime int64 `json:"event_time"` 12 | EventStatus string `json:"event_status"` 13 | AlertKey string `json:"alert_key"` 14 | Labels map[string]string `json:"labels"` 15 | TitleRule string `json:"title_rule"` // $a::b::$c 16 | Description string `json:"description"` 17 | 18 | // for internal use 19 | FirstFireTime int64 `json:"-"` 20 | NotifyCount int64 `json:"-"` 21 | LastSent int64 `json:"-"` 22 | } 23 | 24 | func EventStatusValid(status string) bool { 25 | switch status { 26 | case EventStatusCritical, EventStatusWarning, EventStatusInfo, EventStatusOk: 27 | return true 28 | default: 29 | return false 30 | } 31 | } 32 | 33 | func (e *Event) SetEventStatus(status string) *Event { 34 | e.EventStatus = status 35 | return e 36 | } 37 | 38 | func (e *Event) SetEventTime(t int64) *Event { 39 | e.EventTime = t 40 | return e 41 | } 42 | 43 | func (e *Event) SetTitleRule(rule string) *Event { 44 | e.TitleRule = rule 45 | return e 46 | } 47 | 48 | func (e *Event) SetDescription(desc string) *Event { 49 | e.Description = desc 50 | return e 51 | } 52 | 53 | func BuildEvent(labelMaps ...map[string]string) *Event { 54 | event := &Event{ 55 | EventStatus: EventStatusOk, 56 | } 57 | 58 | event.Labels = make(map[string]string) 59 | for _, labelMap := range labelMaps { 60 | for k, v := range labelMap { 61 | event.Labels[k] = v 62 | } 63 | } 64 | 65 | return event 66 | } 67 | -------------------------------------------------------------------------------- /winx/winx_posix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package winx 4 | 5 | func Args(appPath string) { 6 | } 7 | 8 | func GetServiceName() string { 9 | return "" 10 | } 11 | -------------------------------------------------------------------------------- /winx/winx_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package winx 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/chai2010/winsvc" 11 | ) 12 | 13 | var ( 14 | flagWinSvcName = flag.String("win-service-name", "catpaw", "Set windows service name") 15 | flagWinSvcDesc = flag.String("win-service-desc", "catpaw is a monitoring tool", "Set windows service description") 16 | flagWinSvcInstall = flag.Bool("win-service-install", false, "Install windows service") 17 | flagWinSvcUninstall = flag.Bool("win-service-uninstall", false, "Uninstall windows service") 18 | flagWinSvcStart = flag.Bool("win-service-start", false, "Start windows service") 19 | flagWinSvcStop = flag.Bool("win-service-stop", false, "Stop windows service") 20 | ) 21 | 22 | func GetServiceName() string { 23 | return *flagWinSvcName 24 | } 25 | 26 | func Args(appPath string) { 27 | // install service 28 | if *flagWinSvcInstall { 29 | if err := winsvc.InstallService(appPath, *flagWinSvcName, *flagWinSvcDesc); err != nil { 30 | fmt.Println("failed to install service:", *flagWinSvcName, "error:", err) 31 | } 32 | fmt.Println("done") 33 | os.Exit(0) 34 | } 35 | 36 | // uninstall service 37 | if *flagWinSvcUninstall { 38 | if err := winsvc.RemoveService(*flagWinSvcName); err != nil { 39 | fmt.Println("failed to uninstall service:", *flagWinSvcName, "error:", err) 40 | } 41 | fmt.Println("done") 42 | os.Exit(0) 43 | } 44 | 45 | // start service 46 | if *flagWinSvcStart { 47 | if err := winsvc.StartService(*flagWinSvcName); err != nil { 48 | fmt.Println("failed to start service:", *flagWinSvcName, "error:", err) 49 | } 50 | fmt.Println("done") 51 | os.Exit(0) 52 | } 53 | 54 | // stop service 55 | if *flagWinSvcStop { 56 | if err := winsvc.StopService(*flagWinSvcName); err != nil { 57 | fmt.Println("failed to stop service:", *flagWinSvcName, "error:", err) 58 | } 59 | fmt.Println("done") 60 | os.Exit(0) 61 | } 62 | } 63 | --------------------------------------------------------------------------------