├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .travis.yml ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cmd ├── reporter │ └── reporter.go └── walker │ └── walker.go ├── fswalker.go ├── fswalker_test.go ├── go.mod ├── go.sum ├── internal ├── fsstat │ ├── dev.go │ ├── fsstat_darwin.go │ └── fsstat_linux.go └── metrics │ ├── metrics.go │ └── metrics_test.go ├── proto └── fswalker │ ├── fswalker.pb.go │ └── fswalker.proto ├── reporter.go ├── reporter_test.go ├── testdata ├── defaultClientPolicy.asciipb ├── defaultReportConfig.asciipb ├── hashSumTest └── reviews.asciipb ├── walker.go ├── walker_darwin_test.go ├── walker_linux_test.go └── walker_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | make-test: 9 | name: Unit tests 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: 'stable' 19 | 20 | - name: Run Go unit tests 21 | run: go test ./... 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | git: 3 | depth: 3 4 | language: go 5 | go: 6 | - "master" 7 | - "1.20.x" 8 | - "1.21.x" 9 | env: 10 | global: 11 | - GO111MODULE=on 12 | before_script: 13 | - go get -u golang.org/x/lint/golint 14 | - go mod download -json 15 | script: 16 | - gofmt -d -e -l -s . 17 | - golint -set_exit_status ./... 18 | - go test -v ./... 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # List of code owners and reviewers for github.com/google/fswalker. 2 | 3 | * @google/fswalker @fhchstr @kuuzzzy 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fswalker 2 | 3 | A simple and fast file system integrity checking tool in Go. 4 | 5 | [![Build Status](https://travis-ci.org/google/fswalker.svg?branch=master)](https://travis-ci.org/google/fswalker) 6 | 7 | ## Overview 8 | 9 | fswalker consists of two parts: 10 | 11 | * **Walker**: The walker collects information about the target machine's file 12 | system and writes the collected list out in binary proto format. The walker 13 | policy defines which directories to include and exclude. 14 | 15 | * **Reporter**: The reporter is a tool which runs outside of the target 16 | machine and compares two runs (aka Walks) with each other and reports the 17 | diffs, if any. The report config defines which 18 | directories to include and exclude. 19 | 20 | Note: The walker and the reporter have two separate definitions of directories 21 | to include and exclude. This is done on purpose so more information can be 22 | collected than what is later reviewed. If something suspicious comes up, it is 23 | always possible to see more changes than the ones deemed "interesting" in the 24 | first place. 25 | 26 | Why using fswalker instead of using existing solutions such as Tripwire, 27 | AIDE, Samhain, etc? 28 | 29 | * It's opensource and actively developed. 30 | * All data formats used are open as well and thus allow easy imports and 31 | exports. 32 | * It's easily expandable with local modifications. 33 | * No dependencies on non-standard Go libraries outside github.com/google. 34 | 35 | ## Installation 36 | 37 | ```bash 38 | go get github.com/google/fswalker/cmd/walker 39 | go get github.com/google/fswalker/cmd/reporter 40 | ``` 41 | 42 | ## Configuration 43 | 44 | ### Walker Policy 45 | 46 | The Walker policy specifies how a file system is walked and what to write to the 47 | output file. Most notably, it contains a list of includes and excludes. 48 | 49 | * **include**: Includes are starting points for the file walk. All includes are 50 | walked simultaneously. 51 | 52 | * **exclude_pfx**: Excludes are specified as prefixes. They are literal string 53 | prefix matches. To make this more clear, let's assume we have an `include` of 54 | "/" and an `exclude_pfx` of "/home". When the walker evaluates "/home", it 55 | will skip it because the prefix matches. However, it also skips 56 | "/homeofme/important.file". 57 | 58 | Refer to the proto buffer description to see a complete reference of all 59 | options and their use. 60 | 61 | The following constitutes a functional example for Ubuntu: 62 | 63 | policy.textpb 64 | 65 | ```protobuf 66 | version: 1 67 | max_hash_file_size: 1048576 68 | walk_cross_device: true 69 | ignore_irregular_files: false 70 | include: "/" 71 | exclude_pfx: "/usr/local/" 72 | exclude_pfx: "/usr/src/" 73 | exclude_pfx: "/usr/share/" 74 | exclude_pfx: "/var/backups/" 75 | exclude_pfx: "/var/cache/" 76 | exclude_pfx: "/var/log/" 77 | exclude_pfx: "/var/mail/" 78 | exclude_pfx: "/var/spool/" 79 | exclude_pfx: "/var/tmp/" 80 | ``` 81 | 82 | ### Reporter Config 83 | 84 | The reporter allows to specify fewer things in its config, notably excludes. 85 | The reason to have additional excludes in the reporter is simple: It allows 86 | recording more details in the walks and fewer to be reported. If something 87 | suspicious is ever found, it allows going back to previous walks however and 88 | check what the status was back then. 89 | 90 | * **exclude_pfx**: Excludes are specified as prefixes. They are literal string 91 | prefix matches. To make this more clear, let's assume we have an `include` of 92 | "/" and an `exclude_pfx` of "/home". When the walker evaluates "/home", it 93 | will skip it because the prefix matches. However, it also skips 94 | "/homeofme/important.file". 95 | 96 | The following constitutes a functional example for Ubuntu: 97 | 98 | config.textpb 99 | 100 | ```protobuf 101 | version: 1 102 | exclude_pfx: "/root/" 103 | exclude_pfx: "/home/" 104 | exclude_pfx: "/tmp/" 105 | ``` 106 | 107 | Refer to the proto buffer description to see a complete reference of all 108 | options. 109 | 110 | ### Review File 111 | 112 | The following constitutes a functional example: 113 | 114 | reviews.textpb 115 | 116 | ```protobuf 117 | review: { 118 | key: "some-host.google.com" 119 | value: { 120 | walk_id: "457ab084-2426-4ca8-b54c-cefdce543042" 121 | walk_reference: "/tmp/some-host.google.com-20181205-060000-fswalker-state.pb" 122 | fingerprint: { 123 | method: SHA256 124 | value: "0bfb7506e44dbca14914c3250b2d4d5be005d0de4460c9f298f227bac096f642" 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | Refer to the proto buffer description to see a complete reference of all 131 | options. 132 | 133 | ## Examples 134 | 135 | The following examples show how to run both the walker and the reporter. 136 | 137 | Note that there are libraries for each which can be used independently if so 138 | desired. See the implementations of walker and reporter main for a reference on 139 | how to use the libraries. 140 | 141 | ### Walker 142 | 143 | Once you have a policy as [described above](#walker-policy), you can run the 144 | walker: 145 | 146 | ```bash 147 | walker \ 148 | -policy-file=policy.textpb \ 149 | -output-file-pfx="/tmp" 150 | ``` 151 | 152 | Add `-verbose` to see more details about what's going on. 153 | 154 | ### Reporter 155 | 156 | Once you have a config as [described above](#reporter-config) and more than one 157 | Walk file, you can run the reporter. 158 | 159 | Add `-verbose` to see more details about what's going on. 160 | 161 | To allow for easier reviews, `-paginate` allows to invoke `$PAGER` (or `less` 162 | if `$PAGER` is not set) to page through the results. 163 | 164 | #### Direct Comparison 165 | 166 | The simplest way to run it is to directly specify two Walk files to compare 167 | against each other: 168 | 169 | ```bash 170 | reporter \ 171 | -config-file=config.textpb \ 172 | -before-file=/tmp/some-host.google.com-20181205-060000-fswalker-state.pb \ 173 | -after-file=/tmp/some-host.google.com-20181206-060000-fswalker-state.pb \ 174 | -paginate 175 | ``` 176 | 177 | Note that you can also run with just `-after-file` specified which will basically 178 | list all files as newly added. This is only really useful with a new machine. 179 | 180 | #### Review File Based 181 | 182 | Contrary to the above example, reporter would normally be run with a review 183 | file: 184 | 185 | ```bash 186 | reporter \ 187 | -config-file=config.textpb \ 188 | -review-file=reviews.textpb \ # this needs to be writeable! 189 | -walk-path=/tmp \ 190 | -hostname=some-host.google.com \ 191 | -paginate 192 | ``` 193 | 194 | The reporter runs, displays all diffs and when deemed ok, updates the review file 195 | with the latest "known good" information. 196 | 197 | The idea is that the review file contains a set of "known good" states and is 198 | under version control and four-eye principle / reviews. 199 | 200 | ## Development 201 | 202 | ### Protocol Buffer 203 | 204 | If you change the protocol buffer, ensure you generate a new Go library based on it: 205 | 206 | ```bash 207 | go generate 208 | ``` 209 | 210 | (The rules for `go generate` are in `fswalker.go`.) 211 | 212 | ## License 213 | 214 | Apache 2.0 215 | 216 | This is not an officially supported Google product 217 | -------------------------------------------------------------------------------- /cmd/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Reporter is a CLI tool to process file system report files generated by Walker. 16 | package main 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "fmt" 22 | "io" 23 | "log" 24 | "os" 25 | "os/exec" 26 | "strings" 27 | 28 | "github.com/google/fswalker" 29 | ) 30 | 31 | var ( 32 | configFile = flag.String("config-file", "", "required report config file to use") 33 | walkPath = flag.String("walk-path", "", "path to search for Walks") 34 | reviewFile = flag.String("review-file", "", "path to the file containing a list of last-known-good states - this needs to be writeable") 35 | hostname = flag.String("hostname", "", "host to review the differences for") 36 | beforeFile = flag.String("before-file", "", "path to the file to compare against (last known good typically)") 37 | afterFile = flag.String("after-file", "", "path to the file to compare with the before state") 38 | paginate = flag.Bool("paginate", false, "pipe output into $PAGER in order to paginate and make reviews easier") 39 | verbose = flag.Bool("verbose", false, "print additional output for each file which changed") 40 | updateReview = flag.Bool("update-review", false, "ask to update the \"last known good\" review") 41 | ) 42 | 43 | func askUpdateReviews() bool { 44 | fmt.Print("Do you want to update the \"last known good\" to this [y/N]: ") 45 | var input string 46 | fmt.Scanln(&input) 47 | if strings.ToLower(strings.TrimSpace(input)) == "y" { 48 | return true 49 | } 50 | return false 51 | } 52 | 53 | func walksByLatest(ctx context.Context, r *fswalker.Reporter, hostname, reviewFile, walkPath string) (*fswalker.WalkFile, *fswalker.WalkFile, error) { 54 | before, err := r.ReadLastGoodWalk(ctx, hostname, reviewFile) 55 | if err != nil { 56 | return nil, nil, fmt.Errorf("unable to load last good walk for %s: %v", hostname, err) 57 | } 58 | after, err := r.ReadLatestWalk(ctx, hostname, walkPath) 59 | if err != nil { 60 | return nil, nil, fmt.Errorf("unable to load latest walk for %s: %v", hostname, err) 61 | } 62 | return before, after, nil 63 | } 64 | 65 | func walksByFiles(ctx context.Context, r *fswalker.Reporter, beforeFile, afterFile string) (*fswalker.WalkFile, *fswalker.WalkFile, error) { 66 | after, err := r.ReadWalk(ctx, afterFile) 67 | if err != nil { 68 | return nil, nil, fmt.Errorf("File cannot be read: %s", afterFile) 69 | } 70 | var before *fswalker.WalkFile 71 | if beforeFile != "" { 72 | before, err = r.ReadWalk(ctx, beforeFile) 73 | if err != nil { 74 | return nil, nil, fmt.Errorf("File cannot be read: %s", beforeFile) 75 | } 76 | } 77 | return before, after, nil 78 | } 79 | 80 | func main() { 81 | ctx := context.Background() 82 | flag.Parse() 83 | 84 | // Loading configs and walks. 85 | if *configFile == "" { 86 | log.Fatal("config-file needs to be specified") 87 | } 88 | rptr, err := fswalker.ReporterFromConfigFile(ctx, *configFile, *verbose) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | var before, after *fswalker.WalkFile 94 | var errWalks error 95 | if *hostname != "" && *reviewFile != "" && *walkPath != "" { 96 | if *afterFile != "" || *beforeFile != "" { 97 | log.Fatalf("[hostname review-file walk-path] and [[before-file] after-file] are mutually exclusive") 98 | } 99 | before, after, errWalks = walksByLatest(ctx, rptr, *hostname, *reviewFile, *walkPath) 100 | } else if *afterFile != "" { 101 | before, after, errWalks = walksByFiles(ctx, rptr, *beforeFile, *afterFile) 102 | } else { 103 | log.Fatalf("either [hostname review-file walk-path] OR [[before-file] after-file] need to be specified") 104 | } 105 | if errWalks != nil { 106 | log.Fatal(errWalks) 107 | } 108 | 109 | var report *fswalker.Report 110 | var errReport error 111 | if before == nil { 112 | report, errReport = rptr.Compare(nil, after.Walk) 113 | } else { 114 | report, errReport = rptr.Compare(before.Walk, after.Walk) 115 | } 116 | if errReport != nil { 117 | log.Fatal(errReport) 118 | } 119 | 120 | // Processing and output. 121 | // Note that we do some trickery here to allow pagination via $PAGER if requested. 122 | out := io.WriteCloser(os.Stdout) 123 | var cmd *exec.Cmd 124 | if *paginate { 125 | pager := os.Getenv("PAGER") 126 | if pager == "" { 127 | pager = "/usr/bin/less" 128 | } 129 | // Set up pager piped with the program's stdio. 130 | // Its stdin is closed later in this func, after all reports have been piped. 131 | cmd = exec.Command(pager) 132 | cmd.Stdout = os.Stdout 133 | pipein, err := cmd.StdinPipe() 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | out = pipein 138 | if err := cmd.Start(); err != nil { 139 | log.Fatalf("unable to start %q: %v", pager, err) 140 | } 141 | } 142 | 143 | if before == nil { 144 | fmt.Fprintln(out, "No before walk found. Using after walk only.") 145 | } 146 | rptr.PrintReportSummary(out, report) 147 | if err := rptr.PrintRuleSummary(out, report); err != nil { 148 | log.Fatal(err) 149 | } 150 | rptr.PrintDiffSummary(out, report) 151 | 152 | fmt.Fprintln(out, "Metrics:") 153 | for _, k := range report.Counter.Metrics() { 154 | v, _ := report.Counter.Get(k) 155 | fmt.Fprintf(out, "[%-30s] = %6d\n", k, v) 156 | } 157 | 158 | if *paginate { 159 | out.Close() 160 | cmd.Wait() 161 | } 162 | 163 | // Update reviews file if desired. 164 | if *updateReview && askUpdateReviews() { 165 | if err := rptr.UpdateReviewProto(ctx, after, *reviewFile); err != nil { 166 | log.Fatal(err) 167 | } 168 | } else { 169 | fmt.Println("not updating reviews file") 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /cmd/walker/walker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Walker is a CLI tool to walk over a set of directories and process all discovered files. 16 | package main 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "fmt" 22 | "log" 23 | "os" 24 | "path/filepath" 25 | "time" 26 | 27 | "github.com/google/fswalker" 28 | "google.golang.org/protobuf/proto" 29 | 30 | fspb "github.com/google/fswalker/proto/fswalker" 31 | ) 32 | 33 | var ( 34 | policyFile = flag.String("policy-file", "", "required policy file to use") 35 | outputFilePfx = flag.String("output-file-pfx", "", "path prefix for the output file to write (when a path is set)") 36 | verbose = flag.Bool("verbose", false, "when set to true, prints all discovered files including a metadata summary") 37 | ) 38 | 39 | func walkCallback(ctx context.Context, walk *fspb.Walk) error { 40 | if *outputFilePfx == "" { 41 | return nil 42 | } 43 | outpath, err := outputPath(*outputFilePfx) 44 | if err != nil { 45 | return err 46 | } 47 | walkBytes, err := proto.Marshal(walk) 48 | if err != nil { 49 | return err 50 | } 51 | return fswalker.WriteFile(ctx, outpath, walkBytes, 0444) 52 | } 53 | 54 | func outputPath(pfx string) (string, error) { 55 | hn, err := os.Hostname() 56 | if err != nil { 57 | return "", fmt.Errorf("unable to determine hostname: %v", err) 58 | } 59 | return filepath.Join(pfx, fswalker.WalkFilename(hn, time.Now())), nil 60 | } 61 | 62 | func main() { 63 | ctx := context.Background() 64 | flag.Parse() 65 | 66 | if *policyFile == "" { 67 | log.Fatal("policy-file needs to be specified") 68 | } 69 | 70 | w, err := fswalker.WalkerFromPolicyFile(ctx, *policyFile) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | w.Verbose = *verbose 75 | w.WalkCallback = walkCallback 76 | 77 | // Walk the file system and wait for completion of processing. 78 | if err := w.Run(ctx); err != nil { 79 | log.Fatal(err) 80 | } 81 | 82 | fmt.Println("Metrics:") 83 | for _, k := range w.Counter.Metrics() { 84 | v, _ := w.Counter.Get(k) 85 | fmt.Printf("[%-30s] = %6d\n", k, v) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /fswalker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fswalker contains functionality to walk a file system and compare the differences. 16 | package fswalker 17 | 18 | import ( 19 | "context" 20 | "crypto/sha256" 21 | "encoding/hex" 22 | "fmt" 23 | "io" 24 | "os" 25 | "path/filepath" 26 | "strings" 27 | "time" 28 | 29 | "google.golang.org/protobuf/encoding/prototext" 30 | "google.golang.org/protobuf/proto" 31 | ) 32 | 33 | // Generating Go representations for the proto buf libraries. 34 | 35 | //go:generate protoc -I=. -I=$GOPATH/src --go_out=paths=source_relative:. proto/fswalker/fswalker.proto 36 | 37 | const ( 38 | // tsFileFormat is the time format used in file names. 39 | tsFileFormat = "20060102-150405" 40 | ) 41 | 42 | // WalkFilename returns the appropriate filename for a Walk for the given host and time. 43 | // If time is not provided, it returns a file pattern to glob by. 44 | func WalkFilename(hostname string, t time.Time) string { 45 | hn := "*" 46 | if hostname != "" { 47 | hn = hostname 48 | } 49 | ts := "*" 50 | if !t.IsZero() { 51 | ts = t.Format(tsFileFormat) 52 | } 53 | return fmt.Sprintf("%s-%s-fswalker-state.pb", hn, ts) 54 | } 55 | 56 | // NormalizePath returns a cleaned up path with a path separator at the end if it's a directory. 57 | // It should always be used when printing or comparing paths. 58 | func NormalizePath(path string, isDir bool) string { 59 | p := filepath.Clean(path) 60 | if isDir && p[len(p)-1] != filepath.Separator { 61 | p += string(filepath.Separator) 62 | } 63 | return p 64 | } 65 | 66 | // sha256sum reads the given file path and builds a SHA-256 sum over its content. 67 | func sha256sum(path string) (string, error) { 68 | f, err := os.Open(path) 69 | if err != nil { 70 | return "", err 71 | } 72 | defer f.Close() 73 | 74 | h := sha256.New() 75 | if _, err := io.Copy(h, f); err != nil { 76 | return "", err 77 | } 78 | return hex.EncodeToString(h.Sum(nil)), nil 79 | } 80 | 81 | // readTextProto reads a text format proto buf and unmarshals it into the provided proto message. 82 | func readTextProto(ctx context.Context, path string, pb proto.Message) error { 83 | b, err := ReadFile(ctx, path) 84 | if err != nil { 85 | return err 86 | } 87 | return prototext.Unmarshal(b, pb) 88 | } 89 | 90 | // writeTextProto writes a text format proto buf for the provided proto message. 91 | func writeTextProto(ctx context.Context, path string, pb proto.Message) error { 92 | blob, err := prototext.Marshal(pb) 93 | if err != nil { 94 | return err 95 | } 96 | // replace message boundary characters as curly braces look nicer (both is fine to parse) 97 | blobStr := strings.Replace(strings.Replace(string(blob), "<", "{", -1), ">", "}", -1) 98 | return WriteFile(ctx, path, []byte(blobStr), 0644) 99 | } 100 | 101 | // ReadFile reads the file named by filename and returns the contents. 102 | var ReadFile = func(_ context.Context, filename string) ([]byte, error) { 103 | return os.ReadFile(filename) 104 | } 105 | 106 | // WriteFile writes data to a file named by filename. 107 | var WriteFile = func(_ context.Context, filename string, data []byte, perm os.FileMode) error { 108 | return os.WriteFile(filename, data, perm) 109 | } 110 | 111 | // Glob returns the names of all files matching pattern or nil if there is no matching file. 112 | var Glob = func(_ context.Context, pattern string) ([]string, error) { 113 | return filepath.Glob(pattern) 114 | } 115 | -------------------------------------------------------------------------------- /fswalker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fswalker 16 | 17 | import ( 18 | "context" 19 | "os" 20 | "path/filepath" 21 | "testing" 22 | "time" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | "google.golang.org/protobuf/proto" 26 | 27 | fspb "github.com/google/fswalker/proto/fswalker" 28 | ) 29 | 30 | const ( 31 | testdataDir = "testdata" 32 | ) 33 | 34 | func TestWalkFilename(t *testing.T) { 35 | testCases := []struct { 36 | h string 37 | t time.Time 38 | wantFile string 39 | }{ 40 | { 41 | h: "test-host.google.com", 42 | t: time.Date(2018, 12, 06, 10, 01, 02, 0, time.UTC), 43 | wantFile: "test-host.google.com-20181206-100102-fswalker-state.pb", 44 | }, { 45 | h: "test-host.google.com", 46 | wantFile: "test-host.google.com-*-fswalker-state.pb", 47 | }, { 48 | t: time.Date(2018, 12, 06, 10, 01, 02, 0, time.UTC), 49 | wantFile: "*-20181206-100102-fswalker-state.pb", 50 | }, { 51 | wantFile: "*-*-fswalker-state.pb", 52 | }, 53 | } 54 | 55 | for _, tc := range testCases { 56 | gotFile := WalkFilename(tc.h, tc.t) 57 | if gotFile != tc.wantFile { 58 | t.Errorf("WalkFilename(%s, %s) = %q; want: %q", tc.h, tc.t, gotFile, tc.wantFile) 59 | } 60 | } 61 | } 62 | 63 | func TestNormalizePath(t *testing.T) { 64 | tests := []struct { 65 | // arguments 66 | path string 67 | isDir bool 68 | // expected return value 69 | ret string 70 | }{ 71 | {"/a/b", true, "/a/b/"}, 72 | {"/a/b/", true, "/a/b/"}, 73 | {"/a/b//", true, "/a/b/"}, 74 | {"/a/b", false, "/a/b"}, 75 | {"/a/b/", false, "/a/b"}, 76 | {"/a/b//", false, "/a/b"}, 77 | {"/", false, "/"}, 78 | {"/", true, "/"}, 79 | } 80 | for _, x := range tests { 81 | p := filepath.FromSlash(x.path) 82 | expected := filepath.FromSlash(x.ret) 83 | got := NormalizePath(p, x.isDir) 84 | if got != expected { 85 | t.Errorf("NormalizePath(%q, %v) = %q; want: %q", p, x.isDir, got, expected) 86 | } 87 | } 88 | } 89 | 90 | func TestSha256sum(t *testing.T) { 91 | gotHash, err := sha256sum(filepath.Join(testdataDir, "hashSumTest")) 92 | if err != nil { 93 | t.Errorf("sha256sum() error: %v", err) 94 | return 95 | } 96 | const wantHash = "aeb02544df0ef515b21cab81ad5c0609b774f86879bf7e2e42c88efdaab2c75f" 97 | if gotHash != wantHash { 98 | t.Errorf("sha256sum() = %q; want: %q", gotHash, wantHash) 99 | } 100 | } 101 | 102 | func TestReadTextProtoReviews(t *testing.T) { 103 | ctx := context.Background() 104 | wantReviews := &fspb.Reviews{ 105 | Review: map[string]*fspb.Review{ 106 | "host-A.google.com": { 107 | WalkId: "debffdde-47f3-454b-adaa-d79d95945c69", 108 | WalkReference: "/some/file/path/hostA_20180922_state.pb", 109 | Fingerprint: &fspb.Fingerprint{ 110 | Method: fspb.Fingerprint_SHA256, 111 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 112 | }, 113 | }, 114 | "host-B.google.com": { 115 | WalkId: "2bd40596-d7da-423c-9bb9-c682ebc23f75", 116 | WalkReference: "/some/file/path/hostB_20180810_state.pb", 117 | Fingerprint: &fspb.Fingerprint{ 118 | Method: fspb.Fingerprint_SHA256, 119 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 120 | }, 121 | }, 122 | "host-C.google.com": { 123 | WalkId: "caf8192e-834f-4cd4-a216-fa6f7871ad41", 124 | WalkReference: "/some/file/path/hostC_20180922_state.pb", 125 | Fingerprint: &fspb.Fingerprint{ 126 | Method: fspb.Fingerprint_SHA256, 127 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 128 | }, 129 | }, 130 | }, 131 | } 132 | reviews := &fspb.Reviews{} 133 | if err := readTextProto(ctx, filepath.Join(testdataDir, "reviews.asciipb"), reviews); err != nil { 134 | t.Errorf("readTextProto() error: %v", err) 135 | } 136 | diff := cmp.Diff(reviews, wantReviews, cmp.Comparer(proto.Equal)) 137 | if diff != "" { 138 | t.Errorf("readTextProto(): unexpected content: diff (-want +got):\n%s", diff) 139 | } 140 | } 141 | 142 | func TestReadTextProtoConfigs(t *testing.T) { 143 | ctx := context.Background() 144 | wantConfig := &fspb.ReportConfig{ 145 | Version: 1, 146 | ExcludePfx: []string{ 147 | "/usr/src/linux-headers", 148 | "/usr/share/", 149 | "/proc/", 150 | "/tmp/", 151 | "/var/log/", 152 | "/var/tmp/", 153 | }, 154 | } 155 | config := &fspb.ReportConfig{} 156 | if err := readTextProto(ctx, filepath.Join(testdataDir, "defaultReportConfig.asciipb"), config); err != nil { 157 | t.Fatalf("readTextProto(): %v", err) 158 | } 159 | diff := cmp.Diff(config, wantConfig, cmp.Comparer(proto.Equal)) 160 | if diff != "" { 161 | t.Errorf("readTextProto(): unexpected content: diff (-want +got):\n%s", diff) 162 | } 163 | } 164 | 165 | func TestReadPolicy(t *testing.T) { 166 | wantPol := &fspb.Policy{ 167 | Version: 1, 168 | MaxHashFileSize: 1048576, 169 | Include: []string{ 170 | "/", 171 | }, 172 | ExcludePfx: []string{ 173 | "/usr/src/linux-headers", 174 | "/usr/share/", 175 | "/proc/", 176 | "/sys/", 177 | "/tmp/", 178 | "/var/log/", 179 | "/var/tmp/", 180 | }, 181 | } 182 | ctx := context.Background() 183 | pol := &fspb.Policy{} 184 | if err := readTextProto(ctx, filepath.Join(testdataDir, "defaultClientPolicy.asciipb"), pol); err != nil { 185 | t.Errorf("readTextProto() error: %v", err) 186 | return 187 | } 188 | diff := cmp.Diff(pol, wantPol, cmp.Comparer(proto.Equal)) 189 | if diff != "" { 190 | t.Errorf("readTextProto() policy: diff (-want +got): \n%s", diff) 191 | } 192 | } 193 | 194 | func TestWriteTextProtoReviews(t *testing.T) { 195 | wantReviews := &fspb.Reviews{ 196 | Review: map[string]*fspb.Review{ 197 | "hostname": &fspb.Review{ 198 | WalkId: "id", 199 | WalkReference: "reference", 200 | Fingerprint: &fspb.Fingerprint{ 201 | Method: fspb.Fingerprint_SHA256, 202 | Value: "fingerprint", 203 | }, 204 | }, 205 | }, 206 | } 207 | 208 | tmpfile, err := os.CreateTemp("", "review.asciipb") 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | defer os.Remove(tmpfile.Name()) // clean up 213 | 214 | ctx := context.Background() 215 | if err := writeTextProto(ctx, tmpfile.Name(), wantReviews); err != nil { 216 | t.Errorf("writeTextProto() error: %v", err) 217 | } 218 | 219 | gotReviews := &fspb.Reviews{} 220 | if err := readTextProto(ctx, tmpfile.Name(), gotReviews); err != nil { 221 | t.Errorf("readTextProto() error: %v", err) 222 | } 223 | diff := cmp.Diff(gotReviews, wantReviews, cmp.Comparer(proto.Equal)) 224 | if diff != "" { 225 | t.Errorf("writeTextProto() reviews: diff (-want +got): \n%s", diff) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/fswalker 2 | 3 | go 1.22 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/google/go-cmp v0.7.0 9 | github.com/google/uuid v1.6.0 10 | google.golang.org/protobuf v1.36.6 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 6 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 7 | -------------------------------------------------------------------------------- /internal/fsstat/dev.go: -------------------------------------------------------------------------------- 1 | // Package fsstat provides access to platform specific file stat info. 2 | package fsstat 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "syscall" 8 | 9 | "google.golang.org/protobuf/types/known/timestamppb" 10 | ) 11 | 12 | // DevNumber returns the device number for info 13 | func DevNumber(info os.FileInfo) (uint64, error) { 14 | if stat, ok := info.Sys().(*syscall.Stat_t); ok { 15 | return uint64(stat.Dev), nil 16 | } 17 | 18 | return 0, fmt.Errorf("unable to get file stat for %#v", info) 19 | } 20 | 21 | func timespec2Timestamp(s syscall.Timespec) *timestamppb.Timestamp { 22 | return ×tamppb.Timestamp{Seconds: s.Sec, Nanos: int32(s.Nsec)} 23 | } 24 | -------------------------------------------------------------------------------- /internal/fsstat/fsstat_darwin.go: -------------------------------------------------------------------------------- 1 | package fsstat 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "syscall" 7 | 8 | fspb "github.com/google/fswalker/proto/fswalker" 9 | ) 10 | 11 | // ToStat returns a fspb.ToStat with the file info from the given file 12 | func ToStat(info os.FileInfo) (*fspb.FileStat, error) { 13 | if stat, ok := info.Sys().(*syscall.Stat_t); ok { 14 | return &fspb.FileStat{ 15 | Dev: uint64(stat.Dev), 16 | Inode: stat.Ino, 17 | Nlink: uint64(stat.Nlink), 18 | Mode: uint32(stat.Mode), 19 | Uid: stat.Uid, 20 | Gid: stat.Gid, 21 | Rdev: uint64(stat.Rdev), 22 | Size: stat.Size, 23 | Blksize: int64(stat.Blksize), 24 | Blocks: stat.Blocks, 25 | Atime: timespec2Timestamp(stat.Atimespec), 26 | Mtime: timespec2Timestamp(stat.Mtimespec), 27 | Ctime: timespec2Timestamp(stat.Ctimespec), 28 | }, nil 29 | } 30 | 31 | return nil, fmt.Errorf("unable to get file stat for %#v", info) 32 | } 33 | -------------------------------------------------------------------------------- /internal/fsstat/fsstat_linux.go: -------------------------------------------------------------------------------- 1 | package fsstat 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "syscall" 7 | 8 | fspb "github.com/google/fswalker/proto/fswalker" 9 | ) 10 | 11 | // ToStat returns a fspb.ToStat with the file info from the given file 12 | func ToStat(info os.FileInfo) (*fspb.FileStat, error) { 13 | if stat, ok := info.Sys().(*syscall.Stat_t); ok { 14 | return &fspb.FileStat{ 15 | Dev: uint64(stat.Dev), 16 | Inode: stat.Ino, 17 | Nlink: uint64(stat.Nlink), 18 | Mode: uint32(stat.Mode), 19 | Uid: stat.Uid, 20 | Gid: stat.Gid, 21 | Rdev: uint64(stat.Rdev), 22 | Size: stat.Size, 23 | Blksize: int64(stat.Blksize), 24 | Blocks: stat.Blocks, 25 | Atime: timespec2Timestamp(stat.Atim), 26 | Mtime: timespec2Timestamp(stat.Mtim), 27 | Ctime: timespec2Timestamp(stat.Ctim), 28 | }, nil 29 | } 30 | 31 | return nil, fmt.Errorf("unable to get file stat for %#v", info) 32 | } 33 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package metrics implements generic metrics. 16 | package metrics 17 | 18 | import "sync" 19 | 20 | // Counter keeps count of metrics for parallel running routines. 21 | type Counter struct { 22 | mu sync.RWMutex 23 | counts map[string]int64 24 | } 25 | 26 | // Add adds count to metric. If metric doesn't exist, it creates it. 27 | func (c *Counter) Add(count int64, metric string) { 28 | c.mu.Lock() 29 | defer c.mu.Unlock() 30 | 31 | if c.counts == nil { 32 | c.counts = make(map[string]int64) 33 | } 34 | 35 | c.counts[metric] += count 36 | } 37 | 38 | // Metrics returns a slice of metrics which are tracked. 39 | func (c *Counter) Metrics() []string { 40 | c.mu.RLock() 41 | defer c.mu.RUnlock() 42 | 43 | var metrics []string 44 | for m := range c.counts { 45 | metrics = append(metrics, m) 46 | } 47 | 48 | return metrics 49 | } 50 | 51 | // Get returns the value of a specific metric based on its name as well 52 | // as a bool indicating the value was read successfully. 53 | func (c *Counter) Get(name string) (int64, bool) { 54 | c.mu.RLock() 55 | defer c.mu.RUnlock() 56 | val, ok := c.counts[name] 57 | return val, ok 58 | } 59 | -------------------------------------------------------------------------------- /internal/metrics/metrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package metrics 16 | 17 | import ( 18 | "sync" 19 | "testing" 20 | ) 21 | 22 | func TestCounter(t *testing.T) { 23 | const wantCount = int64(100) 24 | const wantMetric = "test-counter" 25 | c := &Counter{} 26 | 27 | var wg sync.WaitGroup 28 | for i := int64(0); i < wantCount; i++ { 29 | wg.Add(1) 30 | go func() { 31 | c.Add(1, wantMetric) 32 | wg.Done() 33 | }() 34 | } 35 | wg.Wait() 36 | 37 | if n, ok := c.Get(wantMetric); n != wantCount || !ok { 38 | t.Errorf("c.Get(%q) = %d, %v; want %d, true", wantMetric, n, ok, wantCount) 39 | } 40 | 41 | m := c.Metrics() 42 | if len(m) != 1 { 43 | t.Errorf("len(c.Metrics()) = %d; want 1", len(m)) 44 | } 45 | if m[0] != wantMetric { 46 | t.Errorf("c.Metrics()[0] = %q; want %q", m[0], wantMetric) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /proto/fswalker/fswalker.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.31.0 18 | // protoc v4.25.1 19 | // source: fswalker.proto 20 | 21 | package fswalker 22 | 23 | import ( 24 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 25 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 26 | timestamppb "google.golang.org/protobuf/types/known/timestamppb" 27 | reflect "reflect" 28 | sync "sync" 29 | ) 30 | 31 | const ( 32 | // Verify that this generated code is sufficiently up-to-date. 33 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 34 | // Verify that runtime/protoimpl is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 36 | ) 37 | 38 | // Indicator for the severity of the notification. 39 | type Notification_Severity int32 40 | 41 | const ( 42 | Notification_UNKNOWN Notification_Severity = 0 43 | Notification_INFO Notification_Severity = 1 44 | Notification_WARNING Notification_Severity = 2 45 | Notification_ERROR Notification_Severity = 3 46 | ) 47 | 48 | // Enum value maps for Notification_Severity. 49 | var ( 50 | Notification_Severity_name = map[int32]string{ 51 | 0: "UNKNOWN", 52 | 1: "INFO", 53 | 2: "WARNING", 54 | 3: "ERROR", 55 | } 56 | Notification_Severity_value = map[string]int32{ 57 | "UNKNOWN": 0, 58 | "INFO": 1, 59 | "WARNING": 2, 60 | "ERROR": 3, 61 | } 62 | ) 63 | 64 | func (x Notification_Severity) Enum() *Notification_Severity { 65 | p := new(Notification_Severity) 66 | *p = x 67 | return p 68 | } 69 | 70 | func (x Notification_Severity) String() string { 71 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 72 | } 73 | 74 | func (Notification_Severity) Descriptor() protoreflect.EnumDescriptor { 75 | return file_fswalker_proto_enumTypes[0].Descriptor() 76 | } 77 | 78 | func (Notification_Severity) Type() protoreflect.EnumType { 79 | return &file_fswalker_proto_enumTypes[0] 80 | } 81 | 82 | func (x Notification_Severity) Number() protoreflect.EnumNumber { 83 | return protoreflect.EnumNumber(x) 84 | } 85 | 86 | // Deprecated: Use Notification_Severity.Descriptor instead. 87 | func (Notification_Severity) EnumDescriptor() ([]byte, []int) { 88 | return file_fswalker_proto_rawDescGZIP(), []int{5, 0} 89 | } 90 | 91 | type Fingerprint_Method int32 92 | 93 | const ( 94 | Fingerprint_UNKNOWN Fingerprint_Method = 0 95 | Fingerprint_SHA256 Fingerprint_Method = 1 96 | ) 97 | 98 | // Enum value maps for Fingerprint_Method. 99 | var ( 100 | Fingerprint_Method_name = map[int32]string{ 101 | 0: "UNKNOWN", 102 | 1: "SHA256", 103 | } 104 | Fingerprint_Method_value = map[string]int32{ 105 | "UNKNOWN": 0, 106 | "SHA256": 1, 107 | } 108 | ) 109 | 110 | func (x Fingerprint_Method) Enum() *Fingerprint_Method { 111 | p := new(Fingerprint_Method) 112 | *p = x 113 | return p 114 | } 115 | 116 | func (x Fingerprint_Method) String() string { 117 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 118 | } 119 | 120 | func (Fingerprint_Method) Descriptor() protoreflect.EnumDescriptor { 121 | return file_fswalker_proto_enumTypes[1].Descriptor() 122 | } 123 | 124 | func (Fingerprint_Method) Type() protoreflect.EnumType { 125 | return &file_fswalker_proto_enumTypes[1] 126 | } 127 | 128 | func (x Fingerprint_Method) Number() protoreflect.EnumNumber { 129 | return protoreflect.EnumNumber(x) 130 | } 131 | 132 | // Deprecated: Use Fingerprint_Method.Descriptor instead. 133 | func (Fingerprint_Method) EnumDescriptor() ([]byte, []int) { 134 | return file_fswalker_proto_rawDescGZIP(), []int{8, 0} 135 | } 136 | 137 | // Reviews is a collection of "known good" states, one per host. 138 | // It is used to keep the default to compare newer reports against. 139 | type Reviews struct { 140 | state protoimpl.MessageState 141 | sizeCache protoimpl.SizeCache 142 | unknownFields protoimpl.UnknownFields 143 | 144 | Review map[string]*Review `protobuf:"bytes,1,rep,name=review,proto3" json:"review,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Keyed by the FQDN of the host. 145 | } 146 | 147 | func (x *Reviews) Reset() { 148 | *x = Reviews{} 149 | if protoimpl.UnsafeEnabled { 150 | mi := &file_fswalker_proto_msgTypes[0] 151 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 152 | ms.StoreMessageInfo(mi) 153 | } 154 | } 155 | 156 | func (x *Reviews) String() string { 157 | return protoimpl.X.MessageStringOf(x) 158 | } 159 | 160 | func (*Reviews) ProtoMessage() {} 161 | 162 | func (x *Reviews) ProtoReflect() protoreflect.Message { 163 | mi := &file_fswalker_proto_msgTypes[0] 164 | if protoimpl.UnsafeEnabled && x != nil { 165 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 166 | if ms.LoadMessageInfo() == nil { 167 | ms.StoreMessageInfo(mi) 168 | } 169 | return ms 170 | } 171 | return mi.MessageOf(x) 172 | } 173 | 174 | // Deprecated: Use Reviews.ProtoReflect.Descriptor instead. 175 | func (*Reviews) Descriptor() ([]byte, []int) { 176 | return file_fswalker_proto_rawDescGZIP(), []int{0} 177 | } 178 | 179 | func (x *Reviews) GetReview() map[string]*Review { 180 | if x != nil { 181 | return x.Review 182 | } 183 | return nil 184 | } 185 | 186 | type Review struct { 187 | state protoimpl.MessageState 188 | sizeCache protoimpl.SizeCache 189 | unknownFields protoimpl.UnknownFields 190 | 191 | // The ID of the Walk that was reviewed and considered ok. 192 | // This will become the last known good. 193 | WalkId string `protobuf:"bytes,1,opt,name=walk_id,json=walkId,proto3" json:"walk_id,omitempty"` 194 | // Reference to the Walk source (e.g. absolute path). 195 | WalkReference string `protobuf:"bytes,2,opt,name=walk_reference,json=walkReference,proto3" json:"walk_reference,omitempty"` 196 | // Mandatory fingerprint of the walk file (to ensure integrity). 197 | Fingerprint *Fingerprint `protobuf:"bytes,3,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"` 198 | } 199 | 200 | func (x *Review) Reset() { 201 | *x = Review{} 202 | if protoimpl.UnsafeEnabled { 203 | mi := &file_fswalker_proto_msgTypes[1] 204 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 205 | ms.StoreMessageInfo(mi) 206 | } 207 | } 208 | 209 | func (x *Review) String() string { 210 | return protoimpl.X.MessageStringOf(x) 211 | } 212 | 213 | func (*Review) ProtoMessage() {} 214 | 215 | func (x *Review) ProtoReflect() protoreflect.Message { 216 | mi := &file_fswalker_proto_msgTypes[1] 217 | if protoimpl.UnsafeEnabled && x != nil { 218 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 219 | if ms.LoadMessageInfo() == nil { 220 | ms.StoreMessageInfo(mi) 221 | } 222 | return ms 223 | } 224 | return mi.MessageOf(x) 225 | } 226 | 227 | // Deprecated: Use Review.ProtoReflect.Descriptor instead. 228 | func (*Review) Descriptor() ([]byte, []int) { 229 | return file_fswalker_proto_rawDescGZIP(), []int{1} 230 | } 231 | 232 | func (x *Review) GetWalkId() string { 233 | if x != nil { 234 | return x.WalkId 235 | } 236 | return "" 237 | } 238 | 239 | func (x *Review) GetWalkReference() string { 240 | if x != nil { 241 | return x.WalkReference 242 | } 243 | return "" 244 | } 245 | 246 | func (x *Review) GetFingerprint() *Fingerprint { 247 | if x != nil { 248 | return x.Fingerprint 249 | } 250 | return nil 251 | } 252 | 253 | type ReportConfig struct { 254 | state protoimpl.MessageState 255 | sizeCache protoimpl.SizeCache 256 | unknownFields protoimpl.UnknownFields 257 | 258 | // version is the version of the proto structure. 259 | Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` 260 | // exclude_pfx is a list of path prefixes which will be excluded from being 261 | // reported. Note that these are prefixes. Any path matching one of these 262 | // prefixes will be ignored. These are in addition to the exclusions in the 263 | // client policy so more things can be recorded (but ignored in the default 264 | // report). 265 | ExcludePfx []string `protobuf:"bytes,2,rep,name=exclude_pfx,json=excludePfx,proto3" json:"exclude_pfx,omitempty"` 266 | } 267 | 268 | func (x *ReportConfig) Reset() { 269 | *x = ReportConfig{} 270 | if protoimpl.UnsafeEnabled { 271 | mi := &file_fswalker_proto_msgTypes[2] 272 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 273 | ms.StoreMessageInfo(mi) 274 | } 275 | } 276 | 277 | func (x *ReportConfig) String() string { 278 | return protoimpl.X.MessageStringOf(x) 279 | } 280 | 281 | func (*ReportConfig) ProtoMessage() {} 282 | 283 | func (x *ReportConfig) ProtoReflect() protoreflect.Message { 284 | mi := &file_fswalker_proto_msgTypes[2] 285 | if protoimpl.UnsafeEnabled && x != nil { 286 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 287 | if ms.LoadMessageInfo() == nil { 288 | ms.StoreMessageInfo(mi) 289 | } 290 | return ms 291 | } 292 | return mi.MessageOf(x) 293 | } 294 | 295 | // Deprecated: Use ReportConfig.ProtoReflect.Descriptor instead. 296 | func (*ReportConfig) Descriptor() ([]byte, []int) { 297 | return file_fswalker_proto_rawDescGZIP(), []int{2} 298 | } 299 | 300 | func (x *ReportConfig) GetVersion() uint32 { 301 | if x != nil { 302 | return x.Version 303 | } 304 | return 0 305 | } 306 | 307 | func (x *ReportConfig) GetExcludePfx() []string { 308 | if x != nil { 309 | return x.ExcludePfx 310 | } 311 | return nil 312 | } 313 | 314 | type Policy struct { 315 | state protoimpl.MessageState 316 | sizeCache protoimpl.SizeCache 317 | unknownFields protoimpl.UnknownFields 318 | 319 | // version is the version of the proto structure. 320 | Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` 321 | // include is a list of paths to use as roots for file walks. 322 | // Each walk can be done by a separate Go routine (if an idle one is 323 | // available). Important to note that the include paths SHOULD NOT contain 324 | // each other because that will lead to paths being visited more than once. 325 | Include []string `protobuf:"bytes,2,rep,name=include,proto3" json:"include,omitempty"` 326 | // exclude_pfx is a list of path prefixes which will be excluded from being 327 | // walked. Note that these are prefixes. Any path matching one of these 328 | // prefixes will be ignored. 329 | ExcludePfx []string `protobuf:"bytes,3,rep,name=exclude_pfx,json=excludePfx,proto3" json:"exclude_pfx,omitempty"` 330 | // hash_pfx is a list of path prefixes. If the discovered File path is not a 331 | // directory, matches one of the prefixes and is not larger than 332 | // max_hash_file_size, the file will be opened and a file hash built over its 333 | // content. 334 | HashPfx []string `protobuf:"bytes,4,rep,name=hash_pfx,json=hashPfx,proto3" json:"hash_pfx,omitempty"` 335 | MaxHashFileSize int64 `protobuf:"varint,5,opt,name=max_hash_file_size,json=maxHashFileSize,proto3" json:"max_hash_file_size,omitempty"` 336 | // walk_cross_device controls whether files on different devices from the 337 | // include directories should be walked. I.e. if "/" is included, "/tmp" will 338 | // only be walked if it is not a separate mount point. 339 | WalkCrossDevice bool `protobuf:"varint,30,opt,name=walk_cross_device,json=walkCrossDevice,proto3" json:"walk_cross_device,omitempty"` 340 | // ignore_irregular_files controls whether irregular files (i.e. symlinks, 341 | // sockets, devices, etc) should be ignored. 342 | // Note that symlinks are NOT followed either way. 343 | IgnoreIrregularFiles bool `protobuf:"varint,31,opt,name=ignore_irregular_files,json=ignoreIrregularFiles,proto3" json:"ignore_irregular_files,omitempty"` 344 | // max_directory_depth controls how many levels of directories Walker should 345 | // walk into an included directory. 346 | // Defaults to no restriction on depth (i.e. go all the way). 347 | MaxDirectoryDepth uint32 `protobuf:"varint,32,opt,name=max_directory_depth,json=maxDirectoryDepth,proto3" json:"max_directory_depth,omitempty"` 348 | } 349 | 350 | func (x *Policy) Reset() { 351 | *x = Policy{} 352 | if protoimpl.UnsafeEnabled { 353 | mi := &file_fswalker_proto_msgTypes[3] 354 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 355 | ms.StoreMessageInfo(mi) 356 | } 357 | } 358 | 359 | func (x *Policy) String() string { 360 | return protoimpl.X.MessageStringOf(x) 361 | } 362 | 363 | func (*Policy) ProtoMessage() {} 364 | 365 | func (x *Policy) ProtoReflect() protoreflect.Message { 366 | mi := &file_fswalker_proto_msgTypes[3] 367 | if protoimpl.UnsafeEnabled && x != nil { 368 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 369 | if ms.LoadMessageInfo() == nil { 370 | ms.StoreMessageInfo(mi) 371 | } 372 | return ms 373 | } 374 | return mi.MessageOf(x) 375 | } 376 | 377 | // Deprecated: Use Policy.ProtoReflect.Descriptor instead. 378 | func (*Policy) Descriptor() ([]byte, []int) { 379 | return file_fswalker_proto_rawDescGZIP(), []int{3} 380 | } 381 | 382 | func (x *Policy) GetVersion() uint32 { 383 | if x != nil { 384 | return x.Version 385 | } 386 | return 0 387 | } 388 | 389 | func (x *Policy) GetInclude() []string { 390 | if x != nil { 391 | return x.Include 392 | } 393 | return nil 394 | } 395 | 396 | func (x *Policy) GetExcludePfx() []string { 397 | if x != nil { 398 | return x.ExcludePfx 399 | } 400 | return nil 401 | } 402 | 403 | func (x *Policy) GetHashPfx() []string { 404 | if x != nil { 405 | return x.HashPfx 406 | } 407 | return nil 408 | } 409 | 410 | func (x *Policy) GetMaxHashFileSize() int64 { 411 | if x != nil { 412 | return x.MaxHashFileSize 413 | } 414 | return 0 415 | } 416 | 417 | func (x *Policy) GetWalkCrossDevice() bool { 418 | if x != nil { 419 | return x.WalkCrossDevice 420 | } 421 | return false 422 | } 423 | 424 | func (x *Policy) GetIgnoreIrregularFiles() bool { 425 | if x != nil { 426 | return x.IgnoreIrregularFiles 427 | } 428 | return false 429 | } 430 | 431 | func (x *Policy) GetMaxDirectoryDepth() uint32 { 432 | if x != nil { 433 | return x.MaxDirectoryDepth 434 | } 435 | return 0 436 | } 437 | 438 | type Walk struct { 439 | state protoimpl.MessageState 440 | sizeCache protoimpl.SizeCache 441 | unknownFields protoimpl.UnknownFields 442 | 443 | // A unique string identifying this specific Walk. 444 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 445 | // version is the version of the proto structure. 446 | Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` 447 | // policy is the Policy that was used for the file walk. 448 | Policy *Policy `protobuf:"bytes,3,opt,name=policy,proto3" json:"policy,omitempty"` 449 | // file is a list of all files including metadata that were discovered. 450 | File []*File `protobuf:"bytes,4,rep,name=file,proto3" json:"file,omitempty"` 451 | // notification is a list of notifications that occurred during a walk. 452 | Notification []*Notification `protobuf:"bytes,5,rep,name=notification,proto3" json:"notification,omitempty"` 453 | // hostname of the machine the walk originates from. 454 | Hostname string `protobuf:"bytes,10,opt,name=hostname,proto3" json:"hostname,omitempty"` 455 | // start and stop time of the walk. 456 | StartWalk *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=start_walk,json=startWalk,proto3" json:"start_walk,omitempty"` 457 | StopWalk *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=stop_walk,json=stopWalk,proto3" json:"stop_walk,omitempty"` 458 | } 459 | 460 | func (x *Walk) Reset() { 461 | *x = Walk{} 462 | if protoimpl.UnsafeEnabled { 463 | mi := &file_fswalker_proto_msgTypes[4] 464 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 465 | ms.StoreMessageInfo(mi) 466 | } 467 | } 468 | 469 | func (x *Walk) String() string { 470 | return protoimpl.X.MessageStringOf(x) 471 | } 472 | 473 | func (*Walk) ProtoMessage() {} 474 | 475 | func (x *Walk) ProtoReflect() protoreflect.Message { 476 | mi := &file_fswalker_proto_msgTypes[4] 477 | if protoimpl.UnsafeEnabled && x != nil { 478 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 479 | if ms.LoadMessageInfo() == nil { 480 | ms.StoreMessageInfo(mi) 481 | } 482 | return ms 483 | } 484 | return mi.MessageOf(x) 485 | } 486 | 487 | // Deprecated: Use Walk.ProtoReflect.Descriptor instead. 488 | func (*Walk) Descriptor() ([]byte, []int) { 489 | return file_fswalker_proto_rawDescGZIP(), []int{4} 490 | } 491 | 492 | func (x *Walk) GetId() string { 493 | if x != nil { 494 | return x.Id 495 | } 496 | return "" 497 | } 498 | 499 | func (x *Walk) GetVersion() uint32 { 500 | if x != nil { 501 | return x.Version 502 | } 503 | return 0 504 | } 505 | 506 | func (x *Walk) GetPolicy() *Policy { 507 | if x != nil { 508 | return x.Policy 509 | } 510 | return nil 511 | } 512 | 513 | func (x *Walk) GetFile() []*File { 514 | if x != nil { 515 | return x.File 516 | } 517 | return nil 518 | } 519 | 520 | func (x *Walk) GetNotification() []*Notification { 521 | if x != nil { 522 | return x.Notification 523 | } 524 | return nil 525 | } 526 | 527 | func (x *Walk) GetHostname() string { 528 | if x != nil { 529 | return x.Hostname 530 | } 531 | return "" 532 | } 533 | 534 | func (x *Walk) GetStartWalk() *timestamppb.Timestamp { 535 | if x != nil { 536 | return x.StartWalk 537 | } 538 | return nil 539 | } 540 | 541 | func (x *Walk) GetStopWalk() *timestamppb.Timestamp { 542 | if x != nil { 543 | return x.StopWalk 544 | } 545 | return nil 546 | } 547 | 548 | type Notification struct { 549 | state protoimpl.MessageState 550 | sizeCache protoimpl.SizeCache 551 | unknownFields protoimpl.UnknownFields 552 | 553 | Severity Notification_Severity `protobuf:"varint,1,opt,name=severity,proto3,enum=fswalker.Notification_Severity" json:"severity,omitempty"` 554 | // path where the notification occurred. 555 | Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` 556 | // human readable message. 557 | Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` 558 | } 559 | 560 | func (x *Notification) Reset() { 561 | *x = Notification{} 562 | if protoimpl.UnsafeEnabled { 563 | mi := &file_fswalker_proto_msgTypes[5] 564 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 565 | ms.StoreMessageInfo(mi) 566 | } 567 | } 568 | 569 | func (x *Notification) String() string { 570 | return protoimpl.X.MessageStringOf(x) 571 | } 572 | 573 | func (*Notification) ProtoMessage() {} 574 | 575 | func (x *Notification) ProtoReflect() protoreflect.Message { 576 | mi := &file_fswalker_proto_msgTypes[5] 577 | if protoimpl.UnsafeEnabled && x != nil { 578 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 579 | if ms.LoadMessageInfo() == nil { 580 | ms.StoreMessageInfo(mi) 581 | } 582 | return ms 583 | } 584 | return mi.MessageOf(x) 585 | } 586 | 587 | // Deprecated: Use Notification.ProtoReflect.Descriptor instead. 588 | func (*Notification) Descriptor() ([]byte, []int) { 589 | return file_fswalker_proto_rawDescGZIP(), []int{5} 590 | } 591 | 592 | func (x *Notification) GetSeverity() Notification_Severity { 593 | if x != nil { 594 | return x.Severity 595 | } 596 | return Notification_UNKNOWN 597 | } 598 | 599 | func (x *Notification) GetPath() string { 600 | if x != nil { 601 | return x.Path 602 | } 603 | return "" 604 | } 605 | 606 | func (x *Notification) GetMessage() string { 607 | if x != nil { 608 | return x.Message 609 | } 610 | return "" 611 | } 612 | 613 | type FileInfo struct { 614 | state protoimpl.MessageState 615 | sizeCache protoimpl.SizeCache 616 | unknownFields protoimpl.UnknownFields 617 | 618 | // base name of the file 619 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 620 | // length in bytes for regular files; system-dependent for others 621 | Size int64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` 622 | // file mode bits 623 | Mode uint32 `protobuf:"varint,3,opt,name=mode,proto3" json:"mode,omitempty"` 624 | // modification time 625 | Modified *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=modified,proto3" json:"modified,omitempty"` 626 | // abbreviation for Mode().IsDir() 627 | IsDir bool `protobuf:"varint,5,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"` 628 | } 629 | 630 | func (x *FileInfo) Reset() { 631 | *x = FileInfo{} 632 | if protoimpl.UnsafeEnabled { 633 | mi := &file_fswalker_proto_msgTypes[6] 634 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 635 | ms.StoreMessageInfo(mi) 636 | } 637 | } 638 | 639 | func (x *FileInfo) String() string { 640 | return protoimpl.X.MessageStringOf(x) 641 | } 642 | 643 | func (*FileInfo) ProtoMessage() {} 644 | 645 | func (x *FileInfo) ProtoReflect() protoreflect.Message { 646 | mi := &file_fswalker_proto_msgTypes[6] 647 | if protoimpl.UnsafeEnabled && x != nil { 648 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 649 | if ms.LoadMessageInfo() == nil { 650 | ms.StoreMessageInfo(mi) 651 | } 652 | return ms 653 | } 654 | return mi.MessageOf(x) 655 | } 656 | 657 | // Deprecated: Use FileInfo.ProtoReflect.Descriptor instead. 658 | func (*FileInfo) Descriptor() ([]byte, []int) { 659 | return file_fswalker_proto_rawDescGZIP(), []int{6} 660 | } 661 | 662 | func (x *FileInfo) GetName() string { 663 | if x != nil { 664 | return x.Name 665 | } 666 | return "" 667 | } 668 | 669 | func (x *FileInfo) GetSize() int64 { 670 | if x != nil { 671 | return x.Size 672 | } 673 | return 0 674 | } 675 | 676 | func (x *FileInfo) GetMode() uint32 { 677 | if x != nil { 678 | return x.Mode 679 | } 680 | return 0 681 | } 682 | 683 | func (x *FileInfo) GetModified() *timestamppb.Timestamp { 684 | if x != nil { 685 | return x.Modified 686 | } 687 | return nil 688 | } 689 | 690 | func (x *FileInfo) GetIsDir() bool { 691 | if x != nil { 692 | return x.IsDir 693 | } 694 | return false 695 | } 696 | 697 | type FileStat struct { 698 | state protoimpl.MessageState 699 | sizeCache protoimpl.SizeCache 700 | unknownFields protoimpl.UnknownFields 701 | 702 | Dev uint64 `protobuf:"varint,1,opt,name=dev,proto3" json:"dev,omitempty"` 703 | Inode uint64 `protobuf:"varint,2,opt,name=inode,proto3" json:"inode,omitempty"` 704 | Nlink uint64 `protobuf:"varint,3,opt,name=nlink,proto3" json:"nlink,omitempty"` 705 | Mode uint32 `protobuf:"varint,4,opt,name=mode,proto3" json:"mode,omitempty"` 706 | Uid uint32 `protobuf:"varint,5,opt,name=uid,proto3" json:"uid,omitempty"` 707 | Gid uint32 `protobuf:"varint,6,opt,name=gid,proto3" json:"gid,omitempty"` 708 | Rdev uint64 `protobuf:"varint,7,opt,name=rdev,proto3" json:"rdev,omitempty"` 709 | Size int64 `protobuf:"varint,8,opt,name=size,proto3" json:"size,omitempty"` 710 | Blksize int64 `protobuf:"varint,9,opt,name=blksize,proto3" json:"blksize,omitempty"` 711 | Blocks int64 `protobuf:"varint,10,opt,name=blocks,proto3" json:"blocks,omitempty"` 712 | Atime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=atime,proto3" json:"atime,omitempty"` 713 | Mtime *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=mtime,proto3" json:"mtime,omitempty"` 714 | Ctime *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=ctime,proto3" json:"ctime,omitempty"` 715 | } 716 | 717 | func (x *FileStat) Reset() { 718 | *x = FileStat{} 719 | if protoimpl.UnsafeEnabled { 720 | mi := &file_fswalker_proto_msgTypes[7] 721 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 722 | ms.StoreMessageInfo(mi) 723 | } 724 | } 725 | 726 | func (x *FileStat) String() string { 727 | return protoimpl.X.MessageStringOf(x) 728 | } 729 | 730 | func (*FileStat) ProtoMessage() {} 731 | 732 | func (x *FileStat) ProtoReflect() protoreflect.Message { 733 | mi := &file_fswalker_proto_msgTypes[7] 734 | if protoimpl.UnsafeEnabled && x != nil { 735 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 736 | if ms.LoadMessageInfo() == nil { 737 | ms.StoreMessageInfo(mi) 738 | } 739 | return ms 740 | } 741 | return mi.MessageOf(x) 742 | } 743 | 744 | // Deprecated: Use FileStat.ProtoReflect.Descriptor instead. 745 | func (*FileStat) Descriptor() ([]byte, []int) { 746 | return file_fswalker_proto_rawDescGZIP(), []int{7} 747 | } 748 | 749 | func (x *FileStat) GetDev() uint64 { 750 | if x != nil { 751 | return x.Dev 752 | } 753 | return 0 754 | } 755 | 756 | func (x *FileStat) GetInode() uint64 { 757 | if x != nil { 758 | return x.Inode 759 | } 760 | return 0 761 | } 762 | 763 | func (x *FileStat) GetNlink() uint64 { 764 | if x != nil { 765 | return x.Nlink 766 | } 767 | return 0 768 | } 769 | 770 | func (x *FileStat) GetMode() uint32 { 771 | if x != nil { 772 | return x.Mode 773 | } 774 | return 0 775 | } 776 | 777 | func (x *FileStat) GetUid() uint32 { 778 | if x != nil { 779 | return x.Uid 780 | } 781 | return 0 782 | } 783 | 784 | func (x *FileStat) GetGid() uint32 { 785 | if x != nil { 786 | return x.Gid 787 | } 788 | return 0 789 | } 790 | 791 | func (x *FileStat) GetRdev() uint64 { 792 | if x != nil { 793 | return x.Rdev 794 | } 795 | return 0 796 | } 797 | 798 | func (x *FileStat) GetSize() int64 { 799 | if x != nil { 800 | return x.Size 801 | } 802 | return 0 803 | } 804 | 805 | func (x *FileStat) GetBlksize() int64 { 806 | if x != nil { 807 | return x.Blksize 808 | } 809 | return 0 810 | } 811 | 812 | func (x *FileStat) GetBlocks() int64 { 813 | if x != nil { 814 | return x.Blocks 815 | } 816 | return 0 817 | } 818 | 819 | func (x *FileStat) GetAtime() *timestamppb.Timestamp { 820 | if x != nil { 821 | return x.Atime 822 | } 823 | return nil 824 | } 825 | 826 | func (x *FileStat) GetMtime() *timestamppb.Timestamp { 827 | if x != nil { 828 | return x.Mtime 829 | } 830 | return nil 831 | } 832 | 833 | func (x *FileStat) GetCtime() *timestamppb.Timestamp { 834 | if x != nil { 835 | return x.Ctime 836 | } 837 | return nil 838 | } 839 | 840 | // Fingerprint is a unique identifier for a given File. 841 | // It consists of a Method (e.g. SHA256) and a value. 842 | type Fingerprint struct { 843 | state protoimpl.MessageState 844 | sizeCache protoimpl.SizeCache 845 | unknownFields protoimpl.UnknownFields 846 | 847 | Method Fingerprint_Method `protobuf:"varint,1,opt,name=method,proto3,enum=fswalker.Fingerprint_Method" json:"method,omitempty"` 848 | Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` 849 | } 850 | 851 | func (x *Fingerprint) Reset() { 852 | *x = Fingerprint{} 853 | if protoimpl.UnsafeEnabled { 854 | mi := &file_fswalker_proto_msgTypes[8] 855 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 856 | ms.StoreMessageInfo(mi) 857 | } 858 | } 859 | 860 | func (x *Fingerprint) String() string { 861 | return protoimpl.X.MessageStringOf(x) 862 | } 863 | 864 | func (*Fingerprint) ProtoMessage() {} 865 | 866 | func (x *Fingerprint) ProtoReflect() protoreflect.Message { 867 | mi := &file_fswalker_proto_msgTypes[8] 868 | if protoimpl.UnsafeEnabled && x != nil { 869 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 870 | if ms.LoadMessageInfo() == nil { 871 | ms.StoreMessageInfo(mi) 872 | } 873 | return ms 874 | } 875 | return mi.MessageOf(x) 876 | } 877 | 878 | // Deprecated: Use Fingerprint.ProtoReflect.Descriptor instead. 879 | func (*Fingerprint) Descriptor() ([]byte, []int) { 880 | return file_fswalker_proto_rawDescGZIP(), []int{8} 881 | } 882 | 883 | func (x *Fingerprint) GetMethod() Fingerprint_Method { 884 | if x != nil { 885 | return x.Method 886 | } 887 | return Fingerprint_UNKNOWN 888 | } 889 | 890 | func (x *Fingerprint) GetValue() string { 891 | if x != nil { 892 | return x.Value 893 | } 894 | return "" 895 | } 896 | 897 | type File struct { 898 | state protoimpl.MessageState 899 | sizeCache protoimpl.SizeCache 900 | unknownFields protoimpl.UnknownFields 901 | 902 | // version is the version of the proto structure. 903 | Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` 904 | // path is the full file path including the file name. 905 | Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` 906 | Info *FileInfo `protobuf:"bytes,3,opt,name=info,proto3" json:"info,omitempty"` 907 | Stat *FileStat `protobuf:"bytes,4,opt,name=stat,proto3" json:"stat,omitempty"` 908 | // fingerprint is optionally set when requested for the specific file. 909 | Fingerprint []*Fingerprint `protobuf:"bytes,5,rep,name=fingerprint,proto3" json:"fingerprint,omitempty"` 910 | } 911 | 912 | func (x *File) Reset() { 913 | *x = File{} 914 | if protoimpl.UnsafeEnabled { 915 | mi := &file_fswalker_proto_msgTypes[9] 916 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 917 | ms.StoreMessageInfo(mi) 918 | } 919 | } 920 | 921 | func (x *File) String() string { 922 | return protoimpl.X.MessageStringOf(x) 923 | } 924 | 925 | func (*File) ProtoMessage() {} 926 | 927 | func (x *File) ProtoReflect() protoreflect.Message { 928 | mi := &file_fswalker_proto_msgTypes[9] 929 | if protoimpl.UnsafeEnabled && x != nil { 930 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 931 | if ms.LoadMessageInfo() == nil { 932 | ms.StoreMessageInfo(mi) 933 | } 934 | return ms 935 | } 936 | return mi.MessageOf(x) 937 | } 938 | 939 | // Deprecated: Use File.ProtoReflect.Descriptor instead. 940 | func (*File) Descriptor() ([]byte, []int) { 941 | return file_fswalker_proto_rawDescGZIP(), []int{9} 942 | } 943 | 944 | func (x *File) GetVersion() uint32 { 945 | if x != nil { 946 | return x.Version 947 | } 948 | return 0 949 | } 950 | 951 | func (x *File) GetPath() string { 952 | if x != nil { 953 | return x.Path 954 | } 955 | return "" 956 | } 957 | 958 | func (x *File) GetInfo() *FileInfo { 959 | if x != nil { 960 | return x.Info 961 | } 962 | return nil 963 | } 964 | 965 | func (x *File) GetStat() *FileStat { 966 | if x != nil { 967 | return x.Stat 968 | } 969 | return nil 970 | } 971 | 972 | func (x *File) GetFingerprint() []*Fingerprint { 973 | if x != nil { 974 | return x.Fingerprint 975 | } 976 | return nil 977 | } 978 | 979 | var File_fswalker_proto protoreflect.FileDescriptor 980 | 981 | var file_fswalker_proto_rawDesc = []byte{ 982 | 0x0a, 0x0e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 983 | 0x12, 0x08, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 984 | 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 985 | 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8d, 0x01, 0x0a, 0x07, 986 | 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x73, 0x12, 0x35, 0x0a, 0x06, 0x72, 0x65, 0x76, 0x69, 0x65, 987 | 0x77, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 988 | 0x65, 0x72, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x73, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 989 | 0x77, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x1a, 0x4b, 990 | 0x0a, 0x0b, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 991 | 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 992 | 0x26, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 993 | 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 994 | 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x81, 0x01, 0x0a, 0x06, 995 | 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x12, 0x17, 0x0a, 0x07, 0x77, 0x61, 0x6c, 0x6b, 0x5f, 0x69, 996 | 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x77, 0x61, 0x6c, 0x6b, 0x49, 0x64, 0x12, 997 | 0x25, 0x0a, 0x0e, 0x77, 0x61, 0x6c, 0x6b, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 998 | 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x61, 0x6c, 0x6b, 0x52, 0x65, 0x66, 999 | 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 1000 | 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x66, 0x73, 1001 | 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 1002 | 0x6e, 0x74, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x22, 1003 | 0x49, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 1004 | 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 1005 | 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x63, 1006 | 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x66, 0x78, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 1007 | 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x66, 0x78, 0x22, 0xb7, 0x02, 0x0a, 0x06, 0x50, 1008 | 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 1009 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 1010 | 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 1011 | 0x52, 0x07, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x63, 1012 | 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x70, 0x66, 0x78, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 1013 | 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x50, 0x66, 0x78, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 1014 | 0x73, 0x68, 0x5f, 0x70, 0x66, 0x78, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x68, 0x61, 1015 | 0x73, 0x68, 0x50, 0x66, 0x78, 0x12, 0x2b, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x68, 0x61, 0x73, 1016 | 0x68, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 1017 | 0x03, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x48, 0x61, 0x73, 0x68, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 1018 | 0x7a, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6b, 0x5f, 0x63, 0x72, 0x6f, 0x73, 0x73, 1019 | 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x77, 1020 | 0x61, 0x6c, 0x6b, 0x43, 0x72, 0x6f, 0x73, 0x73, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x34, 1021 | 0x0a, 0x16, 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x72, 0x72, 0x65, 0x67, 0x75, 0x6c, 1022 | 0x61, 0x72, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 1023 | 0x69, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x49, 0x72, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x46, 1024 | 0x69, 0x6c, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x6d, 0x61, 0x78, 0x5f, 0x64, 0x69, 0x72, 0x65, 1025 | 0x63, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x64, 0x65, 0x70, 0x74, 0x68, 0x18, 0x20, 0x20, 0x01, 0x28, 1026 | 0x0d, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x44, 1027 | 0x65, 0x70, 0x74, 0x68, 0x22, 0xca, 0x02, 0x0a, 0x04, 0x57, 0x61, 0x6c, 0x6b, 0x12, 0x0e, 0x0a, 1028 | 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 1029 | 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 1030 | 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 1031 | 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 1032 | 0x65, 0x72, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 1033 | 0x79, 0x12, 0x22, 0x0a, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 1034 | 0x0e, 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 1035 | 0x04, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 1036 | 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x73, 1037 | 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 1038 | 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 1039 | 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 1040 | 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 1041 | 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x77, 0x61, 0x6c, 0x6b, 0x18, 0x0b, 0x20, 0x01, 0x28, 1042 | 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 1043 | 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 1044 | 0x74, 0x61, 0x72, 0x74, 0x57, 0x61, 0x6c, 0x6b, 0x12, 0x37, 0x0a, 0x09, 0x73, 0x74, 0x6f, 0x70, 1045 | 0x5f, 0x77, 0x61, 0x6c, 0x6b, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 1046 | 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 1047 | 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x73, 0x74, 0x6f, 0x70, 0x57, 0x61, 0x6c, 1048 | 0x6b, 0x22, 0xb4, 0x01, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 1049 | 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 1050 | 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 1051 | 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x76, 1052 | 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 1053 | 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 1054 | 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 1055 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x39, 0x0a, 1056 | 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 1057 | 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x01, 1058 | 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x09, 0x0a, 1059 | 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x22, 0x95, 0x01, 0x0a, 0x08, 0x46, 0x69, 0x6c, 1060 | 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 1061 | 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 1062 | 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 1063 | 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x6d, 0x6f, 0x64, 1064 | 0x65, 0x12, 0x36, 0x0a, 0x08, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x04, 0x20, 1065 | 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 1066 | 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 1067 | 0x08, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x69, 0x73, 0x5f, 1068 | 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x69, 0x73, 0x44, 0x69, 0x72, 1069 | 0x22, 0xf0, 0x02, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x12, 0x10, 0x0a, 1070 | 0x03, 0x64, 0x65, 0x76, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x64, 0x65, 0x76, 0x12, 1071 | 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 1072 | 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x03, 1073 | 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6d, 1074 | 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 1075 | 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x75, 0x69, 1076 | 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 1077 | 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x64, 0x65, 0x76, 0x18, 0x07, 0x20, 0x01, 0x28, 1078 | 0x04, 0x52, 0x04, 0x72, 0x64, 0x65, 0x76, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 1079 | 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 1080 | 0x6c, 0x6b, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x6c, 1081 | 0x6b, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 1082 | 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x30, 0x0a, 1083 | 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 1084 | 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 1085 | 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 1086 | 0x30, 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 1087 | 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 1088 | 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x6d, 0x74, 0x69, 0x6d, 1089 | 0x65, 0x12, 0x30, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 1090 | 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 1091 | 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x63, 0x74, 1092 | 0x69, 0x6d, 0x65, 0x22, 0x7c, 0x0a, 0x0b, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 1093 | 0x6e, 0x74, 0x12, 0x34, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 1094 | 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x46, 0x69, 1095 | 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 1096 | 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 1097 | 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x21, 1098 | 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 1099 | 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 1100 | 0x01, 0x22, 0xbd, 0x01, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 1101 | 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 1102 | 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 1103 | 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x26, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 1104 | 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 1105 | 0x72, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 1106 | 0x12, 0x26, 0x0a, 0x04, 0x73, 0x74, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 1107 | 0x2e, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x74, 1108 | 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x12, 0x37, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 1109 | 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 1110 | 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x2e, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 1111 | 0x72, 0x69, 0x6e, 0x74, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 1112 | 0x74, 0x42, 0x1c, 0x5a, 0x1a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 1113 | 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x66, 0x73, 0x77, 0x61, 0x6c, 0x6b, 0x65, 0x72, 0x62, 1114 | 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 1115 | } 1116 | 1117 | var ( 1118 | file_fswalker_proto_rawDescOnce sync.Once 1119 | file_fswalker_proto_rawDescData = file_fswalker_proto_rawDesc 1120 | ) 1121 | 1122 | func file_fswalker_proto_rawDescGZIP() []byte { 1123 | file_fswalker_proto_rawDescOnce.Do(func() { 1124 | file_fswalker_proto_rawDescData = protoimpl.X.CompressGZIP(file_fswalker_proto_rawDescData) 1125 | }) 1126 | return file_fswalker_proto_rawDescData 1127 | } 1128 | 1129 | var file_fswalker_proto_enumTypes = make([]protoimpl.EnumInfo, 2) 1130 | var file_fswalker_proto_msgTypes = make([]protoimpl.MessageInfo, 11) 1131 | var file_fswalker_proto_goTypes = []interface{}{ 1132 | (Notification_Severity)(0), // 0: fswalker.Notification.Severity 1133 | (Fingerprint_Method)(0), // 1: fswalker.Fingerprint.Method 1134 | (*Reviews)(nil), // 2: fswalker.Reviews 1135 | (*Review)(nil), // 3: fswalker.Review 1136 | (*ReportConfig)(nil), // 4: fswalker.ReportConfig 1137 | (*Policy)(nil), // 5: fswalker.Policy 1138 | (*Walk)(nil), // 6: fswalker.Walk 1139 | (*Notification)(nil), // 7: fswalker.Notification 1140 | (*FileInfo)(nil), // 8: fswalker.FileInfo 1141 | (*FileStat)(nil), // 9: fswalker.FileStat 1142 | (*Fingerprint)(nil), // 10: fswalker.Fingerprint 1143 | (*File)(nil), // 11: fswalker.File 1144 | nil, // 12: fswalker.Reviews.ReviewEntry 1145 | (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp 1146 | } 1147 | var file_fswalker_proto_depIdxs = []int32{ 1148 | 12, // 0: fswalker.Reviews.review:type_name -> fswalker.Reviews.ReviewEntry 1149 | 10, // 1: fswalker.Review.fingerprint:type_name -> fswalker.Fingerprint 1150 | 5, // 2: fswalker.Walk.policy:type_name -> fswalker.Policy 1151 | 11, // 3: fswalker.Walk.file:type_name -> fswalker.File 1152 | 7, // 4: fswalker.Walk.notification:type_name -> fswalker.Notification 1153 | 13, // 5: fswalker.Walk.start_walk:type_name -> google.protobuf.Timestamp 1154 | 13, // 6: fswalker.Walk.stop_walk:type_name -> google.protobuf.Timestamp 1155 | 0, // 7: fswalker.Notification.severity:type_name -> fswalker.Notification.Severity 1156 | 13, // 8: fswalker.FileInfo.modified:type_name -> google.protobuf.Timestamp 1157 | 13, // 9: fswalker.FileStat.atime:type_name -> google.protobuf.Timestamp 1158 | 13, // 10: fswalker.FileStat.mtime:type_name -> google.protobuf.Timestamp 1159 | 13, // 11: fswalker.FileStat.ctime:type_name -> google.protobuf.Timestamp 1160 | 1, // 12: fswalker.Fingerprint.method:type_name -> fswalker.Fingerprint.Method 1161 | 8, // 13: fswalker.File.info:type_name -> fswalker.FileInfo 1162 | 9, // 14: fswalker.File.stat:type_name -> fswalker.FileStat 1163 | 10, // 15: fswalker.File.fingerprint:type_name -> fswalker.Fingerprint 1164 | 3, // 16: fswalker.Reviews.ReviewEntry.value:type_name -> fswalker.Review 1165 | 17, // [17:17] is the sub-list for method output_type 1166 | 17, // [17:17] is the sub-list for method input_type 1167 | 17, // [17:17] is the sub-list for extension type_name 1168 | 17, // [17:17] is the sub-list for extension extendee 1169 | 0, // [0:17] is the sub-list for field type_name 1170 | } 1171 | 1172 | func init() { file_fswalker_proto_init() } 1173 | func file_fswalker_proto_init() { 1174 | if File_fswalker_proto != nil { 1175 | return 1176 | } 1177 | if !protoimpl.UnsafeEnabled { 1178 | file_fswalker_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 1179 | switch v := v.(*Reviews); i { 1180 | case 0: 1181 | return &v.state 1182 | case 1: 1183 | return &v.sizeCache 1184 | case 2: 1185 | return &v.unknownFields 1186 | default: 1187 | return nil 1188 | } 1189 | } 1190 | file_fswalker_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 1191 | switch v := v.(*Review); i { 1192 | case 0: 1193 | return &v.state 1194 | case 1: 1195 | return &v.sizeCache 1196 | case 2: 1197 | return &v.unknownFields 1198 | default: 1199 | return nil 1200 | } 1201 | } 1202 | file_fswalker_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 1203 | switch v := v.(*ReportConfig); i { 1204 | case 0: 1205 | return &v.state 1206 | case 1: 1207 | return &v.sizeCache 1208 | case 2: 1209 | return &v.unknownFields 1210 | default: 1211 | return nil 1212 | } 1213 | } 1214 | file_fswalker_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 1215 | switch v := v.(*Policy); i { 1216 | case 0: 1217 | return &v.state 1218 | case 1: 1219 | return &v.sizeCache 1220 | case 2: 1221 | return &v.unknownFields 1222 | default: 1223 | return nil 1224 | } 1225 | } 1226 | file_fswalker_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { 1227 | switch v := v.(*Walk); i { 1228 | case 0: 1229 | return &v.state 1230 | case 1: 1231 | return &v.sizeCache 1232 | case 2: 1233 | return &v.unknownFields 1234 | default: 1235 | return nil 1236 | } 1237 | } 1238 | file_fswalker_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { 1239 | switch v := v.(*Notification); i { 1240 | case 0: 1241 | return &v.state 1242 | case 1: 1243 | return &v.sizeCache 1244 | case 2: 1245 | return &v.unknownFields 1246 | default: 1247 | return nil 1248 | } 1249 | } 1250 | file_fswalker_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { 1251 | switch v := v.(*FileInfo); i { 1252 | case 0: 1253 | return &v.state 1254 | case 1: 1255 | return &v.sizeCache 1256 | case 2: 1257 | return &v.unknownFields 1258 | default: 1259 | return nil 1260 | } 1261 | } 1262 | file_fswalker_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { 1263 | switch v := v.(*FileStat); i { 1264 | case 0: 1265 | return &v.state 1266 | case 1: 1267 | return &v.sizeCache 1268 | case 2: 1269 | return &v.unknownFields 1270 | default: 1271 | return nil 1272 | } 1273 | } 1274 | file_fswalker_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { 1275 | switch v := v.(*Fingerprint); i { 1276 | case 0: 1277 | return &v.state 1278 | case 1: 1279 | return &v.sizeCache 1280 | case 2: 1281 | return &v.unknownFields 1282 | default: 1283 | return nil 1284 | } 1285 | } 1286 | file_fswalker_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { 1287 | switch v := v.(*File); i { 1288 | case 0: 1289 | return &v.state 1290 | case 1: 1291 | return &v.sizeCache 1292 | case 2: 1293 | return &v.unknownFields 1294 | default: 1295 | return nil 1296 | } 1297 | } 1298 | } 1299 | type x struct{} 1300 | out := protoimpl.TypeBuilder{ 1301 | File: protoimpl.DescBuilder{ 1302 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 1303 | RawDescriptor: file_fswalker_proto_rawDesc, 1304 | NumEnums: 2, 1305 | NumMessages: 11, 1306 | NumExtensions: 0, 1307 | NumServices: 0, 1308 | }, 1309 | GoTypes: file_fswalker_proto_goTypes, 1310 | DependencyIndexes: file_fswalker_proto_depIdxs, 1311 | EnumInfos: file_fswalker_proto_enumTypes, 1312 | MessageInfos: file_fswalker_proto_msgTypes, 1313 | }.Build() 1314 | File_fswalker_proto = out.File 1315 | file_fswalker_proto_rawDesc = nil 1316 | file_fswalker_proto_goTypes = nil 1317 | file_fswalker_proto_depIdxs = nil 1318 | } 1319 | -------------------------------------------------------------------------------- /proto/fswalker/fswalker.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | option go_package = "github.com/google/fswalker"; 17 | 18 | package fswalker; 19 | 20 | import "google/protobuf/timestamp.proto"; 21 | 22 | // Reviews is a collection of "known good" states, one per host. 23 | // It is used to keep the default to compare newer reports against. 24 | message Reviews { 25 | map review = 1; // Keyed by the FQDN of the host. 26 | } 27 | 28 | message Review { 29 | // The ID of the Walk that was reviewed and considered ok. 30 | // This will become the last known good. 31 | string walk_id = 1; 32 | // Reference to the Walk source (e.g. absolute path). 33 | string walk_reference = 2; 34 | // Mandatory fingerprint of the walk file (to ensure integrity). 35 | Fingerprint fingerprint = 3; 36 | } 37 | 38 | message ReportConfig { 39 | // version is the version of the proto structure. 40 | uint32 version = 1; 41 | 42 | // exclude_pfx is a list of path prefixes which will be excluded from being 43 | // reported. Note that these are prefixes. Any path matching one of these 44 | // prefixes will be ignored. These are in addition to the exclusions in the 45 | // client policy so more things can be recorded (but ignored in the default 46 | // report). 47 | repeated string exclude_pfx = 2; 48 | } 49 | 50 | message Policy { 51 | // version is the version of the proto structure. 52 | uint32 version = 1; 53 | 54 | // include is a list of paths to use as roots for file walks. 55 | // Each walk can be done by a separate Go routine (if an idle one is 56 | // available). Important to note that the include paths SHOULD NOT contain 57 | // each other because that will lead to paths being visited more than once. 58 | repeated string include = 2; 59 | 60 | // exclude_pfx is a list of path prefixes which will be excluded from being 61 | // walked. Note that these are prefixes. Any path matching one of these 62 | // prefixes will be ignored. 63 | repeated string exclude_pfx = 3; 64 | 65 | // hash_pfx is a list of path prefixes. If the discovered File path is not a 66 | // directory, matches one of the prefixes and is not larger than 67 | // max_hash_file_size, the file will be opened and a file hash built over its 68 | // content. 69 | repeated string hash_pfx = 4; 70 | int64 max_hash_file_size = 5; 71 | 72 | // Flags to control general behavior of Walker. 73 | 74 | // walk_cross_device controls whether files on different devices from the 75 | // include directories should be walked. I.e. if "/" is included, "/tmp" will 76 | // only be walked if it is not a separate mount point. 77 | bool walk_cross_device = 30; 78 | // ignore_irregular_files controls whether irregular files (i.e. symlinks, 79 | // sockets, devices, etc) should be ignored. 80 | // Note that symlinks are NOT followed either way. 81 | bool ignore_irregular_files = 31; 82 | // max_directory_depth controls how many levels of directories Walker should 83 | // walk into an included directory. 84 | // Defaults to no restriction on depth (i.e. go all the way). 85 | uint32 max_directory_depth = 32; 86 | } 87 | 88 | message Walk { 89 | // A unique string identifying this specific Walk. 90 | string id = 1; 91 | // version is the version of the proto structure. 92 | uint32 version = 2; 93 | // policy is the Policy that was used for the file walk. 94 | Policy policy = 3; 95 | // file is a list of all files including metadata that were discovered. 96 | repeated File file = 4; 97 | // notification is a list of notifications that occurred during a walk. 98 | repeated Notification notification = 5; 99 | 100 | // hostname of the machine the walk originates from. 101 | string hostname = 10; 102 | // start and stop time of the walk. 103 | google.protobuf.Timestamp start_walk = 11; 104 | google.protobuf.Timestamp stop_walk = 12; 105 | } 106 | 107 | message Notification { 108 | // Indicator for the severity of the notification. 109 | enum Severity { 110 | UNKNOWN = 0; 111 | INFO = 1; 112 | WARNING = 2; 113 | ERROR = 3; 114 | } 115 | Severity severity = 1; 116 | // path where the notification occurred. 117 | string path = 2; 118 | // human readable message. 119 | string message = 3; 120 | } 121 | 122 | // 123 | // The comparison logic might need to be updated if anything below changes. 124 | // 125 | 126 | message FileInfo { 127 | // base name of the file 128 | string name = 1; 129 | // length in bytes for regular files; system-dependent for others 130 | int64 size = 2; 131 | // file mode bits 132 | uint32 mode = 3; 133 | // modification time 134 | google.protobuf.Timestamp modified = 4; 135 | // abbreviation for Mode().IsDir() 136 | bool is_dir = 5; 137 | } 138 | 139 | message FileStat { 140 | uint64 dev = 1; 141 | uint64 inode = 2; 142 | uint64 nlink = 3; 143 | 144 | uint32 mode = 4; 145 | uint32 uid = 5; 146 | uint32 gid = 6; 147 | 148 | uint64 rdev = 7; 149 | int64 size = 8; 150 | int64 blksize = 9; 151 | int64 blocks = 10; 152 | 153 | google.protobuf.Timestamp atime = 11; 154 | google.protobuf.Timestamp mtime = 12; 155 | google.protobuf.Timestamp ctime = 13; 156 | } 157 | 158 | // Fingerprint is a unique identifier for a given File. 159 | // It consists of a Method (e.g. SHA256) and a value. 160 | message Fingerprint { 161 | enum Method { 162 | UNKNOWN = 0; 163 | SHA256 = 1; 164 | } 165 | Method method = 1; 166 | string value = 2; 167 | } 168 | 169 | message File { 170 | // version is the version of the proto structure. 171 | uint32 version = 1; 172 | 173 | // path is the full file path including the file name. 174 | string path = 2; 175 | 176 | FileInfo info = 3; 177 | FileStat stat = 4; 178 | 179 | // fingerprint is optionally set when requested for the specific file. 180 | repeated Fingerprint fingerprint = 5; 181 | } 182 | -------------------------------------------------------------------------------- /reporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fswalker 16 | 17 | import ( 18 | "context" 19 | "crypto/sha256" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "path" 24 | "sort" 25 | "strings" 26 | "time" 27 | 28 | "github.com/google/go-cmp/cmp" 29 | "google.golang.org/protobuf/encoding/prototext" 30 | "google.golang.org/protobuf/proto" 31 | "google.golang.org/protobuf/types/known/timestamppb" 32 | 33 | "github.com/google/fswalker/internal/metrics" 34 | fspb "github.com/google/fswalker/proto/fswalker" 35 | ) 36 | 37 | const ( 38 | timeReportFormat = "2006-01-02 15:04:05 MST" 39 | ) 40 | 41 | // WalkFile contains info about a Walk file. 42 | type WalkFile struct { 43 | Path string 44 | Walk *fspb.Walk 45 | Fingerprint *fspb.Fingerprint 46 | } 47 | 48 | // Report contains the result of the comparison between two Walks. 49 | type Report struct { 50 | Added []ActionData 51 | Deleted []ActionData 52 | Modified []ActionData 53 | Errors []ActionData 54 | Counter *metrics.Counter 55 | WalkBefore *fspb.Walk 56 | WalkAfter *fspb.Walk 57 | } 58 | 59 | // Empty returns true if there are no additions, no deletions, no modifications and no errors. 60 | func (r *Report) Empty() bool { 61 | return len(r.Added)+len(r.Deleted)+len(r.Modified)+len(r.Errors) == 0 62 | } 63 | 64 | // ActionData contains a diff between two files in different Walks. 65 | type ActionData struct { 66 | Before *fspb.File 67 | After *fspb.File 68 | Diff string 69 | Err error 70 | } 71 | 72 | // ReporterFromConfigFile creates a new Reporter based on a config path. 73 | func ReporterFromConfigFile(ctx context.Context, path string, verbose bool) (*Reporter, error) { 74 | config := &fspb.ReportConfig{} 75 | if err := readTextProto(ctx, path, config); err != nil { 76 | return nil, err 77 | } 78 | return &Reporter{ 79 | config: config, 80 | configPath: path, 81 | Verbose: verbose, 82 | }, nil 83 | } 84 | 85 | // Reporter compares two Walks against each other based on the config provided 86 | // and prints a list of diffs between the two. 87 | type Reporter struct { 88 | // config is the configuration defining paths to exclude from the report as well as other aspects. 89 | config *fspb.ReportConfig 90 | configPath string 91 | 92 | // Verbose, when true, makes Reporter print more information for all diffs found. 93 | Verbose bool 94 | } 95 | 96 | func (r *Reporter) verifyFingerprint(goodFp *fspb.Fingerprint, checkFp *fspb.Fingerprint) error { 97 | if checkFp.Method != goodFp.Method { 98 | return fmt.Errorf("fingerprint method %q doesn't match %q", checkFp.Method, goodFp.Method) 99 | } 100 | if goodFp.Method == fspb.Fingerprint_UNKNOWN { 101 | return errors.New("undefined fingerprint method") 102 | } 103 | if goodFp.Value == "" { 104 | return errors.New("empty fingerprint value") 105 | } 106 | if checkFp.Value != goodFp.Value { 107 | return fmt.Errorf("fingerprint %q doesn't match %q", checkFp.Value, goodFp.Value) 108 | } 109 | return nil 110 | } 111 | 112 | func (r *Reporter) fingerprint(b []byte) *fspb.Fingerprint { 113 | v := fmt.Sprintf("%x", sha256.Sum256(b)) 114 | return &fspb.Fingerprint{ 115 | Method: fspb.Fingerprint_SHA256, 116 | Value: v, 117 | } 118 | } 119 | 120 | // ReadWalk reads a file as marshaled proto in fspb.Walk format. 121 | func (r *Reporter) ReadWalk(ctx context.Context, path string) (*WalkFile, error) { 122 | b, err := ReadFile(ctx, path) 123 | if err != nil { 124 | return nil, err 125 | } 126 | p := &fspb.Walk{} 127 | if err := proto.Unmarshal(b, p); err != nil { 128 | return nil, err 129 | } 130 | fp := r.fingerprint(b) 131 | if r.Verbose { 132 | fmt.Printf("Loaded file %q with fingerprint: %s(%s)\n", path, fp.Method, fp.Value) 133 | } 134 | return &WalkFile{Path: path, Walk: p, Fingerprint: fp}, nil 135 | } 136 | 137 | // ReadLatestWalk looks for the latest Walk in a given folder for a given hostname. 138 | // It returns the file path it ended up reading, the Walk it read and the fingerprint for it. 139 | func (r *Reporter) ReadLatestWalk(ctx context.Context, hostname, walkPath string) (*WalkFile, error) { 140 | matchpath := path.Join(walkPath, WalkFilename(hostname, time.Time{})) 141 | names, err := Glob(ctx, matchpath) 142 | if err != nil { 143 | return nil, err 144 | } 145 | if len(names) == 0 { 146 | return nil, fmt.Errorf("no files found for %q", matchpath) 147 | } 148 | sort.Strings(names) // the assumption is that the file names are such that the latest is last. 149 | return r.ReadWalk(ctx, names[len(names)-1]) 150 | } 151 | 152 | // ReadLastGoodWalk reads the designated review file and attempts to find an entry matching 153 | // the given hostname. Note that if it can't find one but the review file itself was read 154 | // successfully, it will return an empty Walk and no error. 155 | // It returns the file path it ended up reading, the Walk it read and the fingerprint for it. 156 | func (r *Reporter) ReadLastGoodWalk(ctx context.Context, hostname, reviewFile string) (*WalkFile, error) { 157 | reviews := &fspb.Reviews{} 158 | if err := readTextProto(ctx, reviewFile, reviews); err != nil { 159 | return nil, err 160 | } 161 | rvws, ok := reviews.Review[hostname] 162 | if !ok { 163 | return nil, nil 164 | } 165 | wf, err := r.ReadWalk(ctx, rvws.WalkReference) 166 | if err != nil { 167 | return wf, err 168 | } 169 | if err := r.verifyFingerprint(rvws.Fingerprint, wf.Fingerprint); err != nil { 170 | return wf, err 171 | } 172 | if wf.Walk.Id != rvws.WalkId { 173 | return wf, fmt.Errorf("walk ID doesn't match: %s (from %s) != %s (from %s)", wf.Walk.Id, rvws.WalkReference, rvws.WalkId, reviewFile) 174 | } 175 | return wf, nil 176 | } 177 | 178 | // ErrSameWalks is returned when comparing a walk with the same walk. 179 | var ErrSameWalks = fmt.Errorf("Walks are the same") 180 | 181 | // sanityCheck runs a few checks to ensure the "before" and "after" Walks are sane-ish. 182 | func (r *Reporter) sanityCheck(before, after *fspb.Walk) error { 183 | if after == nil { 184 | return fmt.Errorf("either hostname, reviewFile and walkPath OR at least afterFile need to be specified") 185 | } 186 | if before != nil && before.Id == after.Id { 187 | return fmt.Errorf("ID of both Walks is %s: %w", before.Id, ErrSameWalks) 188 | } 189 | if before != nil && before.Version != after.Version { 190 | return fmt.Errorf("versions don't match: before(%d) != after(%d)", before.Version, after.Version) 191 | } 192 | if before != nil && before.Hostname != after.Hostname { 193 | return fmt.Errorf("you're comparing apples and oranges: %s != %s", before.Hostname, after.Hostname) 194 | } 195 | if before != nil { 196 | beforeTs := before.StopWalk.AsTime() 197 | afterTs := after.StartWalk.AsTime() 198 | if beforeTs.After(afterTs) { 199 | return fmt.Errorf("earlier Walk indicates it ended (%s) after later Walk (%s) has started", beforeTs, afterTs) 200 | } 201 | } 202 | return nil 203 | } 204 | 205 | // isIgnored checks for a given file path whether it is ignored by the report config or not. 206 | func (r *Reporter) isIgnored(path string) bool { 207 | for _, i := range r.config.ExcludePfx { 208 | if strings.HasPrefix(path, i) { 209 | return true 210 | } 211 | } 212 | return false 213 | } 214 | 215 | func (r *Reporter) timestampDiff(bt, at *timestamppb.Timestamp) (string, error) { 216 | if bt == nil && at == nil { 217 | return "", nil 218 | } 219 | bmt := bt.AsTime() 220 | amt := at.AsTime() 221 | 222 | if bmt.Equal(amt) { 223 | return "", nil 224 | } 225 | return fmt.Sprintf("%s => %s", bmt.Format(timeReportFormat), amt.Format(timeReportFormat)), nil 226 | } 227 | 228 | // diffFileStat compares the FileInfo proto of two files and reports all relevant diffs as human readable strings. 229 | func (r *Reporter) diffFileInfo(fib, fia *fspb.FileInfo) ([]string, error) { 230 | var diffs []string 231 | 232 | if fib == nil && fia == nil { 233 | return diffs, nil 234 | } 235 | 236 | if fib.Name != fia.Name { 237 | diffs = append(diffs, fmt.Sprintf("name: %q => %q", fib.Name, fia.Name)) 238 | } 239 | if fib.Size != fia.Size { 240 | diffs = append(diffs, fmt.Sprintf("size: %d => %d", fib.Size, fia.Size)) 241 | } 242 | if fib.Mode != fia.Mode { 243 | diffs = append(diffs, fmt.Sprintf("mode: %d => %d", fib.Mode, fia.Mode)) 244 | } 245 | if fib.IsDir != fia.IsDir { 246 | diffs = append(diffs, fmt.Sprintf("is_dir: %t => %t", fib.IsDir, fia.IsDir)) 247 | } 248 | 249 | // Ignore if both timestamps are nil. 250 | if fib.Modified == nil && fia.Modified == nil { 251 | return diffs, nil 252 | } 253 | diff, err := r.timestampDiff(fib.Modified, fia.Modified) 254 | if err != nil { 255 | return diffs, fmt.Errorf("unable to convert timestamps for %q: %v", fib.Name, err) 256 | } 257 | if diff != "" { 258 | diffs = append(diffs, fmt.Sprintf("mtime: %s", diff)) 259 | } 260 | 261 | return diffs, nil 262 | } 263 | 264 | // diffFileStat compares the FileStat proto of two files and reports all relevant diffs as human readable strings. 265 | // The following fields are ignored as they are not regarded as relevant in this context: 266 | // - atime 267 | // - inode, nlink, dev, rdev 268 | // - blksize, blocks 269 | // 270 | // The following fields are ignored as they are already part of diffFileInfo() check 271 | // which is more guaranteed to be available (to avoid duplicate output): 272 | // - mode 273 | // - size 274 | // - mtime 275 | func (r *Reporter) diffFileStat(fsb, fsa *fspb.FileStat) ([]string, error) { 276 | var diffs []string 277 | 278 | if fsb == nil && fsa == nil { 279 | return diffs, nil 280 | } 281 | 282 | if fsb.Uid != fsa.Uid { 283 | diffs = append(diffs, fmt.Sprintf("uid: %d => %d", fsb.Uid, fsa.Uid)) 284 | } 285 | if fsb.Gid != fsa.Gid { 286 | diffs = append(diffs, fmt.Sprintf("gid: %d => %d", fsb.Gid, fsa.Gid)) 287 | } 288 | 289 | // Ignore ctime changes if mtime equals to ctime or if both are nil. 290 | cdiff, cerr := r.timestampDiff(fsb.Ctime, fsa.Ctime) 291 | if cerr != nil { 292 | return diffs, fmt.Errorf("unable to convert timestamps: %v", cerr) 293 | } 294 | if cdiff == "" { 295 | return diffs, nil 296 | } 297 | mdiff, merr := r.timestampDiff(fsb.Mtime, fsa.Mtime) 298 | if merr != nil { 299 | return diffs, fmt.Errorf("unable to convert timestamps: %v", merr) 300 | } 301 | if mdiff != cdiff { 302 | diffs = append(diffs, fmt.Sprintf("ctime: %s", cdiff)) 303 | } 304 | 305 | return diffs, nil 306 | } 307 | 308 | // diffFile compares two File entries of a Walk and shows the diffs between the two. 309 | func (r *Reporter) diffFile(before, after *fspb.File) (string, error) { 310 | if before.Version != after.Version { 311 | return "", fmt.Errorf("file format versions don't match: before(%d) != after(%d)", before.Version, after.Version) 312 | } 313 | if before.Path != after.Path { 314 | return "", fmt.Errorf("file paths don't match: before(%q) != after(%q)", before.Path, after.Path) 315 | } 316 | 317 | var diffs []string 318 | // Ensure fingerprints are the same - if there was one before. Do not show a diff if there's a new fingerprint. 319 | if len(before.Fingerprint) > 0 { 320 | fb := before.Fingerprint[0] 321 | if len(after.Fingerprint) == 0 { 322 | diffs = append(diffs, fmt.Sprintf("fingerprint: %s => ", fb.Value)) 323 | } else { 324 | fa := after.Fingerprint[0] 325 | if fb.Method != fa.Method { 326 | diffs = append(diffs, fmt.Sprintf("fingerprint-method: %s => %s", fb.Method, fa.Method)) 327 | } 328 | if fb.Value != fa.Value { 329 | diffs = append(diffs, fmt.Sprintf("fingerprint: %s => %s", fb.Value, fa.Value)) 330 | } 331 | } 332 | } 333 | fiDiffs, err := r.diffFileInfo(before.Info, after.Info) 334 | if err != nil { 335 | return "", fmt.Errorf("unable to diff file info for %q: %v", before.Path, err) 336 | } 337 | diffs = append(diffs, fiDiffs...) 338 | fsDiffs, err := r.diffFileStat(before.Stat, after.Stat) 339 | if err != nil { 340 | return "", fmt.Errorf("unable to diff file stat for %q: %v", before.Path, err) 341 | } 342 | diffs = append(diffs, fsDiffs...) 343 | sort.Strings(diffs) 344 | return strings.Join(diffs, "\n"), nil 345 | } 346 | 347 | // Compare two Walks and returns the diffs. 348 | func (r *Reporter) Compare(before, after *fspb.Walk) (*Report, error) { 349 | if err := r.sanityCheck(before, after); err != nil { 350 | return nil, err 351 | } 352 | 353 | walkedBefore := map[string]*fspb.File{} 354 | walkedAfter := map[string]*fspb.File{} 355 | if before != nil { 356 | for _, fbOrig := range before.File { 357 | fb := proto.Clone(fbOrig).(*fspb.File) 358 | fb.Path = NormalizePath(fb.Path, fb.Info.IsDir) 359 | walkedBefore[fb.Path] = fb 360 | } 361 | } 362 | for _, faOrig := range after.File { 363 | fa := proto.Clone(faOrig).(*fspb.File) 364 | fa.Path = NormalizePath(fa.Path, fa.Info.IsDir) 365 | walkedAfter[fa.Path] = fa 366 | } 367 | 368 | counter := metrics.Counter{} 369 | output := Report{ 370 | Counter: &counter, 371 | WalkBefore: before, 372 | WalkAfter: after, 373 | } 374 | 375 | for _, fb := range walkedBefore { 376 | counter.Add(1, "before-files") 377 | if r.isIgnored(fb.Path) { 378 | counter.Add(1, "before-files-ignored") 379 | continue 380 | } 381 | fa := walkedAfter[fb.Path] 382 | if fa == nil { 383 | counter.Add(1, "before-files-removed") 384 | output.Deleted = append(output.Deleted, ActionData{Before: fb}) 385 | continue 386 | } 387 | diff, err := r.diffFile(fb, fa) 388 | if err != nil { 389 | counter.Add(1, "file-diff-error") 390 | output.Errors = append(output.Errors, ActionData{ 391 | Before: fb, 392 | After: fa, 393 | Diff: diff, 394 | Err: err, 395 | }) 396 | } 397 | if diff != "" { 398 | counter.Add(1, "before-files-modified") 399 | output.Modified = append(output.Modified, ActionData{ 400 | Before: fb, 401 | After: fa, 402 | Diff: diff, 403 | }) 404 | } 405 | } 406 | for _, fa := range walkedAfter { 407 | counter.Add(1, "after-files") 408 | if r.isIgnored(fa.Path) { 409 | counter.Add(1, "after-files-ignored") 410 | continue 411 | } 412 | _, ok := walkedBefore[fa.Path] 413 | if ok { 414 | continue 415 | } 416 | counter.Add(1, "after-files-created") 417 | output.Added = append(output.Added, ActionData{After: fa}) 418 | } 419 | sort.Slice(output.Added, func(i, j int) bool { 420 | return output.Added[i].After.Path < output.Added[j].After.Path 421 | }) 422 | sort.Slice(output.Deleted, func(i, j int) bool { 423 | return output.Deleted[i].Before.Path < output.Deleted[j].Before.Path 424 | }) 425 | sort.Slice(output.Modified, func(i, j int) bool { 426 | return output.Modified[i].Before.Path < output.Modified[j].Before.Path 427 | }) 428 | sort.Slice(output.Errors, func(i, j int) bool { 429 | return output.Errors[i].Before.Path < output.Errors[j].Before.Path 430 | }) 431 | return &output, nil 432 | } 433 | 434 | // PrintDiffSummary prints the diffs found in a Report. 435 | func (r *Reporter) PrintDiffSummary(out io.Writer, report *Report) { 436 | fmt.Fprintln(out, "===============================================================================") 437 | fmt.Fprintln(out, "Object Summary:") 438 | fmt.Fprintln(out, "===============================================================================") 439 | 440 | if len(report.Added) > 0 { 441 | fmt.Fprintf(out, "Added (%d):\n", len(report.Added)) 442 | for _, file := range report.Added { 443 | fmt.Fprintln(out, file.After.Path) 444 | } 445 | fmt.Fprintln(out) 446 | } 447 | if len(report.Deleted) > 0 { 448 | fmt.Fprintf(out, "Removed (%d):\n", len(report.Deleted)) 449 | for _, file := range report.Deleted { 450 | fmt.Fprintln(out, file.Before.Path) 451 | } 452 | fmt.Fprintln(out) 453 | } 454 | if len(report.Modified) > 0 { 455 | fmt.Fprintf(out, "Modified (%d):\n", len(report.Modified)) 456 | for _, file := range report.Modified { 457 | fmt.Fprintln(out, file.After.Path) 458 | if r.Verbose { 459 | fmt.Fprintln(out, file.Diff) 460 | fmt.Fprintln(out) 461 | } 462 | } 463 | fmt.Fprintln(out) 464 | } 465 | if len(report.Errors) > 0 { 466 | fmt.Fprintf(out, "Reporting Errors (%d):\n", len(report.Errors)) 467 | for _, file := range report.Errors { 468 | fmt.Fprintf(out, "%s: %v\n", file.Before.Path, file.Err) 469 | } 470 | fmt.Fprintln(out) 471 | } 472 | if report.Empty() { 473 | fmt.Fprintln(out, "No changes.") 474 | } 475 | if report.WalkBefore != nil && len(report.WalkBefore.Notification) > 0 { 476 | fmt.Fprintln(out, "Walking Errors for BEFORE file:") 477 | for _, err := range report.WalkBefore.Notification { 478 | if r.Verbose || (err.Severity != fspb.Notification_UNKNOWN && err.Severity != fspb.Notification_INFO) { 479 | fmt.Fprintf(out, "%s(%s): %s\n", err.Severity, err.Path, err.Message) 480 | } 481 | } 482 | fmt.Fprintln(out) 483 | } 484 | if len(report.WalkAfter.Notification) > 0 { 485 | fmt.Fprintln(out, "Walking Errors for AFTER file:") 486 | for _, err := range report.WalkAfter.Notification { 487 | if r.Verbose || (err.Severity != fspb.Notification_UNKNOWN && err.Severity != fspb.Notification_INFO) { 488 | fmt.Fprintf(out, "%s(%s): %s\n", err.Severity, err.Path, err.Message) 489 | } 490 | } 491 | fmt.Fprintln(out) 492 | } 493 | } 494 | 495 | // printWalkSummary prints some information about the given walk. 496 | func (r *Reporter) printWalkSummary(out io.Writer, walk *fspb.Walk) { 497 | awst := walk.StartWalk.AsTime() 498 | awet := walk.StopWalk.AsTime() 499 | fmt.Fprintf(out, " - ID: %s\n", walk.Id) 500 | fmt.Fprintf(out, " - Start Time: %s\n", awst) 501 | fmt.Fprintf(out, " - Stop Time: %s\n", awet) 502 | } 503 | 504 | // PrintReportSummary prints a few key information pieces around the Report. 505 | func (r *Reporter) PrintReportSummary(out io.Writer, report *Report) { 506 | fmt.Fprintln(out, "===============================================================================") 507 | fmt.Fprintln(out, "Report Summary:") 508 | fmt.Fprintln(out, "===============================================================================") 509 | fmt.Fprintf(out, "Host name: %s\n", report.WalkAfter.Hostname) 510 | fmt.Fprintf(out, "Report config used: %s\n", r.configPath) 511 | if report.WalkBefore != nil { 512 | fmt.Fprintln(out, "Walk (Before)") 513 | r.printWalkSummary(out, report.WalkBefore) 514 | } 515 | fmt.Fprintln(out, "Walk (After)") 516 | r.printWalkSummary(out, report.WalkAfter) 517 | fmt.Fprintln(out) 518 | } 519 | 520 | // PrintRuleSummary prints the configs and policies involved in creating the Walk and Report. 521 | func (r *Reporter) PrintRuleSummary(out io.Writer, report *Report) error { 522 | fmt.Fprintln(out, "===============================================================================") 523 | fmt.Fprintln(out, "Rule Summary:") 524 | fmt.Fprintln(out, "===============================================================================") 525 | 526 | if report.WalkBefore != nil { 527 | diff := cmp.Diff(report.WalkBefore.Policy, report.WalkAfter.Policy, cmp.Comparer(proto.Equal)) 528 | if diff != "" { 529 | fmt.Fprintln(out, "Walks policy diff:") 530 | fmt.Fprintln(out, diff) 531 | } else { 532 | fmt.Fprintln(out, "No changes.") 533 | } 534 | } 535 | if r.Verbose { 536 | policy := report.WalkAfter.Policy 537 | if report.WalkBefore != nil { 538 | policy = report.WalkBefore.Policy 539 | } 540 | 541 | policyBytes, err := prototext.Marshal(policy) 542 | if err != nil { 543 | return err 544 | } 545 | configBytes, err := prototext.Marshal(r.config) 546 | if err != nil { 547 | return err 548 | } 549 | fmt.Fprintf(out, "Policy:\n%s\nReport Config:\n%s\n", string(policyBytes), string(configBytes)) 550 | } 551 | return nil 552 | } 553 | 554 | // UpdateReviewProto updates the reviews file to the reviewed version to be "last known good". 555 | func (r *Reporter) UpdateReviewProto(ctx context.Context, walkFile *WalkFile, reviewFile string) error { 556 | review := &fspb.Review{ 557 | WalkId: walkFile.Walk.Id, 558 | WalkReference: walkFile.Path, 559 | Fingerprint: walkFile.Fingerprint, 560 | } 561 | blob, err := prototext.Marshal(&fspb.Reviews{ 562 | Review: map[string]*fspb.Review{ 563 | walkFile.Walk.Hostname: review, 564 | }, 565 | }) 566 | if err != nil { 567 | return err 568 | } 569 | blobStr := string(blob) 570 | fmt.Println("New review section:") 571 | // replace message boundary characters as curly braces look nicer (both is fine to parse) 572 | fmt.Println(strings.Replace(strings.Replace(blobStr, "<", "{", -1), ">", "}", -1)) 573 | 574 | if reviewFile != "" { 575 | reviews := &fspb.Reviews{} 576 | if err := readTextProto(ctx, reviewFile, reviews); err != nil { 577 | return err 578 | } 579 | 580 | reviews.Review[walkFile.Walk.Hostname] = review 581 | if err := writeTextProto(ctx, reviewFile, reviews); err != nil { 582 | return err 583 | } 584 | fmt.Printf("Changes written to %q\n", reviewFile) 585 | } else { 586 | fmt.Println("No reviews file provided so you will have to update it manually.") 587 | } 588 | return nil 589 | } 590 | -------------------------------------------------------------------------------- /reporter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fswalker 16 | 17 | import ( 18 | "context" 19 | "crypto/sha256" 20 | "fmt" 21 | "os" 22 | "testing" 23 | "time" 24 | 25 | "github.com/google/go-cmp/cmp" 26 | "github.com/google/go-cmp/cmp/cmpopts" 27 | "google.golang.org/protobuf/proto" 28 | "google.golang.org/protobuf/types/known/timestamppb" 29 | 30 | fspb "github.com/google/fswalker/proto/fswalker" 31 | ) 32 | 33 | func TestVerifyFingerprint(t *testing.T) { 34 | testCases := []struct { 35 | desc string 36 | goodFp *fspb.Fingerprint 37 | checkFp *fspb.Fingerprint 38 | wantErr bool 39 | }{ 40 | { 41 | desc: "pass with all good values", 42 | goodFp: &fspb.Fingerprint{ 43 | Method: fspb.Fingerprint_SHA256, 44 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 45 | }, 46 | checkFp: &fspb.Fingerprint{ 47 | Method: fspb.Fingerprint_SHA256, 48 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 49 | }, 50 | wantErr: false, 51 | }, { 52 | desc: "pass but not ok with all different values", 53 | goodFp: &fspb.Fingerprint{ 54 | Method: fspb.Fingerprint_SHA256, 55 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 56 | }, 57 | checkFp: &fspb.Fingerprint{ 58 | Method: fspb.Fingerprint_SHA256, 59 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7484", 60 | }, 61 | wantErr: true, 62 | }, { 63 | desc: "fail with different fingerprinting methods", 64 | goodFp: &fspb.Fingerprint{ 65 | Method: fspb.Fingerprint_SHA256, 66 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 67 | }, 68 | checkFp: &fspb.Fingerprint{ 69 | Method: fspb.Fingerprint_UNKNOWN, 70 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 71 | }, 72 | wantErr: true, 73 | }, { 74 | desc: "fail with unknown fingerprinting method", 75 | goodFp: &fspb.Fingerprint{ 76 | Method: fspb.Fingerprint_UNKNOWN, 77 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 78 | }, 79 | checkFp: &fspb.Fingerprint{ 80 | Method: fspb.Fingerprint_UNKNOWN, 81 | Value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483", 82 | }, 83 | wantErr: true, 84 | }, { 85 | desc: "fail with empty fingerprint value", 86 | goodFp: &fspb.Fingerprint{ 87 | Method: fspb.Fingerprint_SHA256, 88 | Value: "", 89 | }, 90 | checkFp: &fspb.Fingerprint{ 91 | Method: fspb.Fingerprint_SHA256, 92 | Value: "", 93 | }, 94 | wantErr: true, 95 | }, 96 | } 97 | 98 | for _, tc := range testCases { 99 | r := &Reporter{} 100 | t.Run(tc.desc, func(t *testing.T) { 101 | err := r.verifyFingerprint(tc.goodFp, tc.checkFp) 102 | switch { 103 | case tc.wantErr && err == nil: 104 | t.Error("verifyFingerprint() returned nil error") 105 | case !tc.wantErr && err != nil: 106 | t.Errorf("verifyFingerprint(): %v", err) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestFingerprint(t *testing.T) { 113 | b := []byte("test string") 114 | wantFp := "d5579c46dfcc7f18207013e65b44e4cb4e2c2298f4ac457ba8f82743f31e930b" 115 | r := &Reporter{} 116 | fp := r.fingerprint(b) 117 | if fp.Method != fspb.Fingerprint_SHA256 { 118 | t.Errorf("fingerprint().Method: got=%v, want=SHA256", fp.Value) 119 | } 120 | if fp.Value != wantFp { 121 | t.Errorf("fingerprint().Value: got=%s, want=%s", fp.Value, wantFp) 122 | } 123 | } 124 | 125 | func TestReadWalk(t *testing.T) { 126 | ctx := context.Background() 127 | wantWalk := &fspb.Walk{ 128 | Id: "", 129 | Version: 1, 130 | Hostname: "testhost", 131 | StartWalk: timestamppb.Now(), 132 | StopWalk: timestamppb.Now(), 133 | Policy: &fspb.Policy{ 134 | Version: 1, 135 | Include: []string{ 136 | "/", 137 | }, 138 | ExcludePfx: []string{ 139 | "/var/log/", 140 | "/home/", 141 | "/tmp/", 142 | }, 143 | HashPfx: []string{ 144 | "/etc/", 145 | }, 146 | MaxHashFileSize: 1024 * 1024, 147 | }, 148 | File: []*fspb.File{ 149 | { 150 | Version: 1, 151 | Path: "/etc/test", 152 | Info: &fspb.FileInfo{ 153 | Name: "hashSumTest", 154 | Size: 100, 155 | Mode: 640, 156 | IsDir: false, 157 | }, 158 | Fingerprint: []*fspb.Fingerprint{ 159 | { 160 | Method: fspb.Fingerprint_SHA256, 161 | Value: "deadbeef", 162 | }, 163 | }, 164 | }, 165 | }, 166 | } 167 | 168 | walkBytes, err := proto.Marshal(wantWalk) 169 | if err != nil { 170 | t.Fatalf("problems marshaling walk: %v", err) 171 | } 172 | 173 | tmpfile, err := os.CreateTemp("", "walk.pb") 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | defer os.Remove(tmpfile.Name()) // clean up 178 | if _, err := tmpfile.Write(walkBytes); err != nil { 179 | t.Fatal(err) 180 | } 181 | if err := tmpfile.Close(); err != nil { 182 | t.Fatal(err) 183 | } 184 | 185 | h := sha256.New() 186 | h.Write(walkBytes) 187 | wantFp := fmt.Sprintf("%x", h.Sum(nil)) 188 | 189 | r := &Reporter{} 190 | got, err := r.ReadWalk(ctx, tmpfile.Name()) 191 | if err != nil { 192 | t.Fatalf("readwalk(): %v", err) 193 | } 194 | if got.Fingerprint.Method != fspb.Fingerprint_SHA256 { 195 | t.Errorf("readwalk(): fingerprint method, got=%v, want=SHA256", got.Fingerprint.Method) 196 | } 197 | if got.Fingerprint.Value != wantFp { 198 | t.Errorf("readwalk(): fingerprint value, got=%s, want=%s", got.Fingerprint.Value, wantFp) 199 | } 200 | diff := cmp.Diff(got.Walk, wantWalk, cmp.Comparer(proto.Equal)) 201 | if diff != "" { 202 | t.Errorf("readwalk(): content diff (-want +got):\n%s", diff) 203 | } 204 | } 205 | 206 | func TestSanityCheck(t *testing.T) { 207 | ts1 := timestamppb.Now() 208 | ts2 := timestamppb.New(time.Now().Add(time.Hour * 10)) 209 | ts3 := timestamppb.New(time.Now().Add(time.Hour * 20)) 210 | testCases := []struct { 211 | before *fspb.Walk 212 | after *fspb.Walk 213 | wantErr error 214 | }{ 215 | { 216 | before: &fspb.Walk{}, 217 | after: &fspb.Walk{}, 218 | wantErr: ErrSameWalks, 219 | }, { 220 | before: nil, 221 | after: &fspb.Walk{}, 222 | wantErr: nil, 223 | }, { 224 | before: &fspb.Walk{}, 225 | after: nil, 226 | wantErr: cmpopts.AnyError, 227 | }, { 228 | before: &fspb.Walk{}, 229 | after: &fspb.Walk{ 230 | Id: "unique2", 231 | }, 232 | wantErr: nil, 233 | }, { 234 | before: &fspb.Walk{ 235 | Id: "unique1", 236 | Version: 1, 237 | Hostname: "testhost1", 238 | StartWalk: ts1, 239 | StopWalk: ts1, 240 | }, 241 | after: &fspb.Walk{ 242 | Id: "unique2", 243 | Version: 1, 244 | Hostname: "testhost1", 245 | StartWalk: ts2, 246 | StopWalk: ts3, 247 | }, 248 | wantErr: nil, 249 | }, { 250 | before: nil, 251 | after: &fspb.Walk{ 252 | Id: "unique2", 253 | Version: 1, 254 | Hostname: "testhost1", 255 | StartWalk: ts2, 256 | StopWalk: ts3, 257 | }, 258 | wantErr: nil, 259 | }, { 260 | before: &fspb.Walk{ 261 | Id: "unique1", 262 | Version: 1, 263 | }, 264 | after: &fspb.Walk{ 265 | Id: "unique2", 266 | Version: 2, 267 | }, 268 | wantErr: cmpopts.AnyError, 269 | }, { 270 | before: &fspb.Walk{ 271 | Id: "unique1", 272 | StartWalk: ts1, 273 | StopWalk: ts2, 274 | }, 275 | after: &fspb.Walk{ 276 | Id: "unique2", 277 | StartWalk: ts1, 278 | StopWalk: ts3, 279 | }, 280 | wantErr: cmpopts.AnyError, 281 | }, { 282 | before: &fspb.Walk{ 283 | Id: "unique1", 284 | Hostname: "testhost1", 285 | }, 286 | after: &fspb.Walk{ 287 | Id: "unique2", 288 | Hostname: "testhost2", 289 | }, 290 | wantErr: cmpopts.AnyError, 291 | }, 292 | } 293 | 294 | for _, tc := range testCases { 295 | r := &Reporter{} 296 | err := r.sanityCheck(tc.before, tc.after) 297 | if !cmp.Equal(tc.wantErr, err, cmpopts.EquateErrors()) { 298 | t.Errorf("sanityCheck() = %v, want %v", err, tc.wantErr) 299 | } 300 | } 301 | } 302 | 303 | func TestIsIgnored(t *testing.T) { 304 | conf := &fspb.ReportConfig{ 305 | Version: 1, 306 | ExcludePfx: []string{ 307 | "/tmp/", 308 | "/var/log/", 309 | }, 310 | } 311 | testCases := []struct { 312 | path string 313 | wantIg bool 314 | }{ 315 | { 316 | path: "/tmp/something", 317 | wantIg: true, 318 | }, { 319 | path: "/tmp/", 320 | wantIg: true, 321 | }, { 322 | path: "/tmp", 323 | wantIg: false, 324 | }, { 325 | path: "/tmp2/file", 326 | wantIg: false, 327 | }, { 328 | path: "/home/someone", 329 | wantIg: false, 330 | }, 331 | } 332 | 333 | for _, tc := range testCases { 334 | t.Run(tc.path, func(t *testing.T) { 335 | r := &Reporter{ 336 | config: conf, 337 | } 338 | gotIg := r.isIgnored(tc.path) 339 | if gotIg != tc.wantIg { 340 | t.Errorf("isIgnored() ignore: got=%t, want=%t", gotIg, tc.wantIg) 341 | } 342 | }) 343 | } 344 | } 345 | 346 | func TestDiffFile(t *testing.T) { 347 | testCases := []struct { 348 | desc string 349 | before *fspb.File 350 | after *fspb.File 351 | wantDiff string 352 | wantErr bool 353 | }{ 354 | { 355 | desc: "same empty files", 356 | before: &fspb.File{}, 357 | after: &fspb.File{}, 358 | wantDiff: "", 359 | }, { 360 | desc: "same non-empty files", 361 | before: &fspb.File{ 362 | Version: 1, 363 | Path: "/tmp/testfile", 364 | Info: &fspb.FileInfo{ 365 | Size: 1000, 366 | Mode: 644, 367 | Modified: ×tamppb.Timestamp{}, 368 | }, 369 | }, 370 | after: &fspb.File{ 371 | Version: 1, 372 | Path: "/tmp/testfile", 373 | Info: &fspb.FileInfo{ 374 | Size: 1000, 375 | Mode: 644, 376 | Modified: ×tamppb.Timestamp{}, 377 | }, 378 | }, 379 | wantDiff: "", 380 | }, { 381 | desc: "file info changes mode and mtime", 382 | before: &fspb.File{ 383 | Version: 1, 384 | Path: "/tmp/testfile", 385 | Info: &fspb.FileInfo{ 386 | Size: 1000, 387 | Mode: 644, 388 | Modified: ×tamppb.Timestamp{ 389 | Seconds: int64(1543831000), 390 | }, 391 | }, 392 | }, 393 | after: &fspb.File{ 394 | Version: 1, 395 | Path: "/tmp/testfile", 396 | Info: &fspb.FileInfo{ 397 | Size: 1000, 398 | Mode: 744, 399 | Modified: ×tamppb.Timestamp{ 400 | Seconds: int64(1543931000), 401 | }, 402 | }, 403 | }, 404 | wantDiff: "mode: 644 => 744\nmtime: 2018-12-03 09:56:40 UTC => 2018-12-04 13:43:20 UTC", 405 | }, { 406 | desc: "file stat changes uid and ctime", 407 | before: &fspb.File{ 408 | Version: 1, 409 | Path: "/tmp/testfile", 410 | Stat: &fspb.FileStat{ 411 | Uid: uint32(5000), 412 | Ctime: ×tamppb.Timestamp{ 413 | Seconds: int64(1543831000), 414 | }, 415 | }, 416 | }, 417 | after: &fspb.File{ 418 | Version: 1, 419 | Path: "/tmp/testfile", 420 | Stat: &fspb.FileStat{ 421 | Uid: uint32(0), 422 | Ctime: ×tamppb.Timestamp{ 423 | Seconds: int64(1543931000), 424 | }, 425 | }, 426 | }, 427 | wantDiff: "ctime: 2018-12-03 09:56:40 UTC => 2018-12-04 13:43:20 UTC\nuid: 5000 => 0", 428 | }, { 429 | desc: "file changes version", 430 | before: &fspb.File{ 431 | Version: 1, 432 | Path: "/tmp/testfile", 433 | Info: &fspb.FileInfo{ 434 | Size: 1000, 435 | Mode: 644, 436 | }, 437 | }, 438 | after: &fspb.File{ 439 | Version: 2, 440 | Path: "/tmp/testfile", 441 | Info: &fspb.FileInfo{ 442 | Size: 1000, 443 | Mode: 644, 444 | }, 445 | }, 446 | wantErr: true, 447 | }, { 448 | desc: "no fingerprint after", 449 | before: &fspb.File{ 450 | Path: "/tmp/testfile", 451 | Fingerprint: []*fspb.Fingerprint{&fspb.Fingerprint{Value: "abcd"}}, 452 | }, 453 | after: &fspb.File{ 454 | Path: "/tmp/testfile", 455 | }, 456 | wantDiff: "fingerprint: abcd => ", 457 | }, { 458 | desc: "diff fingerprints", 459 | before: &fspb.File{ 460 | Path: "/tmp/testfile", 461 | Fingerprint: []*fspb.Fingerprint{&fspb.Fingerprint{Value: "abcd"}}, 462 | }, 463 | after: &fspb.File{ 464 | Path: "/tmp/testfile", 465 | Fingerprint: []*fspb.Fingerprint{&fspb.Fingerprint{Value: "efgh"}}, 466 | }, 467 | wantDiff: "fingerprint: abcd => efgh", 468 | }, { 469 | desc: "fingerprint only after", 470 | before: &fspb.File{ 471 | Path: "/tmp/testfile", 472 | }, 473 | after: &fspb.File{ 474 | Path: "/tmp/testfile", 475 | Fingerprint: []*fspb.Fingerprint{&fspb.Fingerprint{Value: "abcd"}}, 476 | }, 477 | wantDiff: "", 478 | }, 479 | } 480 | 481 | for _, tc := range testCases { 482 | t.Run(tc.desc, func(t *testing.T) { 483 | r := &Reporter{} 484 | gotDiff, err := r.diffFile(tc.before, tc.after) 485 | switch { 486 | case tc.wantErr && err == nil: 487 | t.Error("diffFile() no error") 488 | case !tc.wantErr && err != nil: 489 | t.Errorf("diffFile() error: %v", err) 490 | default: 491 | if gotDiff != tc.wantDiff { 492 | t.Errorf("diffFile() diff: got=%q, want=%q", gotDiff, tc.wantDiff) 493 | } 494 | } 495 | }) 496 | } 497 | } 498 | 499 | func TestCompare(t *testing.T) { 500 | testCases := []struct { 501 | desc string 502 | before *fspb.Walk 503 | after *fspb.Walk 504 | deleted int 505 | added int 506 | modified int 507 | wantError bool 508 | }{ 509 | { 510 | desc: "nil before", 511 | before: nil, 512 | after: &fspb.Walk{ 513 | File: []*fspb.File{ 514 | &fspb.File{Path: "/a/b/c", Info: &fspb.FileInfo{}}, 515 | }, 516 | }, 517 | added: 1, 518 | }, { 519 | desc: "empty after", 520 | before: &fspb.Walk{ 521 | Id: "1", 522 | File: []*fspb.File{ 523 | &fspb.File{Path: "/a/b/c", Info: &fspb.FileInfo{}}, 524 | }, 525 | }, 526 | after: &fspb.Walk{Id: "2"}, 527 | deleted: 1, 528 | }, { 529 | desc: "nil before and after", 530 | before: nil, 531 | after: nil, 532 | wantError: true, 533 | }, { 534 | desc: "diffs", 535 | before: &fspb.Walk{ 536 | Id: "1", 537 | File: []*fspb.File{ 538 | &fspb.File{Path: "/a/b/c", Info: &fspb.FileInfo{}}, 539 | &fspb.File{Path: "/e/f/g", Info: &fspb.FileInfo{Size: 4}}, 540 | &fspb.File{Path: "/x/y/z", Info: &fspb.FileInfo{}}, 541 | }, 542 | }, 543 | after: &fspb.Walk{ 544 | Id: "2", 545 | File: []*fspb.File{ 546 | &fspb.File{Path: "/b/c/d", Info: &fspb.FileInfo{}}, 547 | &fspb.File{Path: "/e/f/g", Info: &fspb.FileInfo{Size: 7}}, 548 | &fspb.File{Path: "/x/y/z", Info: &fspb.FileInfo{}}, 549 | }, 550 | }, 551 | added: 1, 552 | deleted: 1, 553 | modified: 1, 554 | }, { 555 | desc: "ignore", 556 | before: &fspb.Walk{ 557 | Id: "1", 558 | File: []*fspb.File{ 559 | &fspb.File{Path: "/ignore/a", Info: &fspb.FileInfo{}}, 560 | }, 561 | }, 562 | after: &fspb.Walk{ 563 | Id: "2", 564 | File: []*fspb.File{ 565 | &fspb.File{Path: "/ignore/b", Info: &fspb.FileInfo{}}, 566 | }, 567 | }, 568 | }, { 569 | desc: "same dir with and without trailing /", 570 | before: &fspb.Walk{ 571 | Id: "1", 572 | File: []*fspb.File{ 573 | &fspb.File{Path: "/a/b/c/", Info: &fspb.FileInfo{IsDir: true}}, 574 | }, 575 | }, 576 | after: &fspb.Walk{ 577 | Id: "2", 578 | File: []*fspb.File{ 579 | &fspb.File{Path: "/a/b/c", Info: &fspb.FileInfo{IsDir: true}}, 580 | }, 581 | }, 582 | }, 583 | } 584 | for _, tc := range testCases { 585 | t.Run(tc.desc, func(t *testing.T) { 586 | r := &Reporter{config: &fspb.ReportConfig{ExcludePfx: []string{"/ignore/"}}} 587 | report, err := r.Compare(tc.before, tc.after) 588 | switch { 589 | case tc.wantError && err == nil: 590 | t.Error("Compare() no error") 591 | case !tc.wantError && err != nil: 592 | t.Errorf("Compare() error: %v", err) 593 | case err == nil: 594 | if n := len(report.Added); n != tc.added { 595 | t.Errorf("len(report.Added) = %d; want %d", n, tc.added) 596 | } 597 | if n := len(report.Deleted); n != tc.deleted { 598 | t.Errorf("len(report.Deleted) = %d; want %d", n, tc.deleted) 599 | } 600 | if n := len(report.Modified); n != tc.modified { 601 | t.Errorf("len(report.Modified) = %d; want %d", n, tc.modified) 602 | } 603 | } 604 | }) 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /testdata/defaultClientPolicy.asciipb: -------------------------------------------------------------------------------- 1 | version: 1 2 | max_hash_file_size: 1048576 3 | include: "/" 4 | exclude_pfx: "/usr/src/linux-headers" 5 | exclude_pfx: "/usr/share/" 6 | exclude_pfx: "/proc/" 7 | exclude_pfx: "/sys/" 8 | exclude_pfx: "/tmp/" 9 | exclude_pfx: "/var/log/" 10 | exclude_pfx: "/var/tmp/" 11 | -------------------------------------------------------------------------------- /testdata/defaultReportConfig.asciipb: -------------------------------------------------------------------------------- 1 | version: 1 2 | exclude_pfx: "/usr/src/linux-headers" 3 | exclude_pfx: "/usr/share/" 4 | exclude_pfx: "/proc/" 5 | exclude_pfx: "/tmp/" 6 | exclude_pfx: "/var/log/" 7 | exclude_pfx: "/var/tmp/" 8 | -------------------------------------------------------------------------------- /testdata/hashSumTest: -------------------------------------------------------------------------------- 1 | DO NOT EDIT THIS FILE -------------------------------------------------------------------------------- /testdata/reviews.asciipb: -------------------------------------------------------------------------------- 1 | review: { 2 | key: "host-A.google.com" 3 | value: { 4 | walk_id: "debffdde-47f3-454b-adaa-d79d95945c69" 5 | walk_reference: "/some/file/path/hostA_20180922_state.pb" 6 | fingerprint: { 7 | method: SHA256 8 | value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483" 9 | } 10 | } 11 | } 12 | review: { 13 | key: "host-B.google.com" 14 | value: { 15 | walk_id: "2bd40596-d7da-423c-9bb9-c682ebc23f75" 16 | walk_reference: "/some/file/path/hostB_20180810_state.pb" 17 | fingerprint: { 18 | method: SHA256 19 | value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483" 20 | } 21 | } 22 | } 23 | review: { 24 | key: "host-C.google.com" 25 | value: { 26 | walk_id: "caf8192e-834f-4cd4-a216-fa6f7871ad41" 27 | walk_reference: "/some/file/path/hostC_20180922_state.pb" 28 | fingerprint: { 29 | method: SHA256 30 | value: "5669df6b2f003ca61714b1b9830c41cf3a2ebe644abb2516db3021c20a1b7483" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /walker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fswalker 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "log" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | "sync" 25 | 26 | "github.com/google/uuid" 27 | "google.golang.org/protobuf/proto" 28 | "google.golang.org/protobuf/types/known/timestamppb" 29 | 30 | "github.com/google/fswalker/internal/fsstat" 31 | "github.com/google/fswalker/internal/metrics" 32 | fspb "github.com/google/fswalker/proto/fswalker" 33 | ) 34 | 35 | const ( 36 | // Number of root paths to walk in parallel. 37 | parallelism = 1 38 | 39 | // Versions for compatibility comparison. 40 | fileVersion = 1 41 | walkVersion = 1 42 | 43 | // Unique names for each counter - used by the counter output processor. 44 | countFiles = "file-count" 45 | countDirectories = "dir-count" 46 | countFileSizeSum = "file-size-sum" 47 | countStatErr = "file-stat-errors" 48 | countHashes = "file-hash-count" 49 | ) 50 | 51 | // WalkCallback is called by Walker at the end of the Run. 52 | // The callback is typically used to dump the walk to disk and/or perform any other checks. 53 | // The error return value is propagated back to the Run callers. 54 | type WalkCallback func(context.Context, *fspb.Walk) error 55 | 56 | // WalkerFromPolicyFile creates a new Walker based on a policy path. 57 | func WalkerFromPolicyFile(ctx context.Context, path string) (*Walker, error) { 58 | pol := &fspb.Policy{} 59 | if err := readTextProto(ctx, path, pol); err != nil { 60 | return nil, err 61 | } 62 | return &Walker{ 63 | pol: pol, 64 | Counter: &metrics.Counter{}, 65 | }, nil 66 | } 67 | 68 | // Walker is able to walk a file structure starting with a list of given includes 69 | // as roots. All paths starting with any prefix specified in the excludes are 70 | // ignored. The list of specific files in the hash list are read and a hash sum 71 | // built for each. Note that this is expensive and should not be done for large 72 | // files or a large number of files. 73 | type Walker struct { 74 | // pol is the configuration defining which paths to include and exclude from the walk. 75 | pol *fspb.Policy 76 | 77 | // walk collects all processed files during a run. 78 | walk *fspb.Walk 79 | walkMu sync.Mutex 80 | 81 | // Function to call once the Walk is complete i.e. to inspect or write the Walk. 82 | WalkCallback WalkCallback 83 | 84 | // Verbose, when true, makes Walker print file metadata to stdout. 85 | Verbose bool 86 | 87 | // Counter records stats over all processed files, if non-nil. 88 | Counter *metrics.Counter 89 | } 90 | 91 | // convert creates a File from the given information and if requested embeds the hash sum too. 92 | func (w *Walker) convert(path string, info os.FileInfo) (*fspb.File, error) { 93 | path = filepath.Clean(path) 94 | 95 | f := &fspb.File{ 96 | Version: fileVersion, 97 | Path: path, 98 | } 99 | 100 | if info == nil { 101 | return f, nil 102 | } 103 | 104 | var shaSum string 105 | // Only build the hash sum if requested and if it is not a directory. 106 | if w.wantHashing(path) && !info.IsDir() && info.Size() <= w.pol.MaxHashFileSize { 107 | var err error 108 | shaSum, err = sha256sum(path) 109 | if err != nil { 110 | log.Printf("unable to build hash for %s: %s", path, err) 111 | } else { 112 | f.Fingerprint = []*fspb.Fingerprint{ 113 | { 114 | Method: fspb.Fingerprint_SHA256, 115 | Value: shaSum, 116 | }, 117 | } 118 | } 119 | } 120 | 121 | mts := timestamppb.New(info.ModTime()) 122 | f.Info = &fspb.FileInfo{ 123 | Name: info.Name(), 124 | Size: info.Size(), 125 | Mode: uint32(info.Mode()), 126 | Modified: mts, 127 | IsDir: info.IsDir(), 128 | } 129 | 130 | var err error 131 | if f.Stat, err = fsstat.ToStat(info); err != nil { 132 | return nil, err 133 | } 134 | 135 | return f, nil 136 | } 137 | 138 | // wantHashing determines whether the given path was asked to be hashed. 139 | func (w *Walker) wantHashing(path string) bool { 140 | for _, p := range w.pol.HashPfx { 141 | if strings.HasPrefix(path, p) { 142 | return true 143 | } 144 | } 145 | return false 146 | } 147 | 148 | // isExcluded determines whether a given path was asked to be excluded from scanning. 149 | func (w *Walker) isExcluded(path string) bool { 150 | for _, e := range w.pol.ExcludePfx { 151 | if strings.HasPrefix(path, e) { 152 | return true 153 | } 154 | } 155 | return false 156 | } 157 | 158 | // process runs output functions for the given input File. 159 | func (w *Walker) process(ctx context.Context, f *fspb.File) error { 160 | // Print a short overview if we're running in verbose mode. 161 | if w.Verbose { 162 | fmt.Println(NormalizePath(f.Path, f.Info.IsDir)) 163 | ts := proto.Clone(f.Info.Modified) 164 | info := []string{ 165 | fmt.Sprintf("size(%d)", f.Info.Size), 166 | fmt.Sprintf("mode(%v)", os.FileMode(f.Info.Mode)), 167 | fmt.Sprintf("mTime(%v)", ts), 168 | fmt.Sprintf("uid(%d)", f.Stat.Uid), 169 | fmt.Sprintf("gid(%d)", f.Stat.Gid), 170 | fmt.Sprintf("inode(%d)", f.Stat.Inode), 171 | } 172 | for _, fp := range f.Fingerprint { 173 | info = append(info, fmt.Sprintf("%s(%s)", fspb.Fingerprint_Method_name[int32(fp.Method)], fp.Value)) 174 | } 175 | fmt.Println(strings.Join(info, ", ")) 176 | } 177 | 178 | // Add file to the walk which will later be written out to disk. 179 | w.addFileToWalk(f) 180 | 181 | // Collect some metrics. 182 | if w.Counter != nil { 183 | if f.Info.IsDir { 184 | w.Counter.Add(1, countDirectories) 185 | } else { 186 | w.Counter.Add(1, countFiles) 187 | } 188 | w.Counter.Add(f.Info.Size, countFileSizeSum) 189 | if f.Stat == nil { 190 | w.Counter.Add(1, countStatErr) 191 | } 192 | if len(f.Fingerprint) > 0 { 193 | w.Counter.Add(1, countHashes) 194 | } 195 | } 196 | 197 | return nil 198 | } 199 | 200 | func (w *Walker) addFileToWalk(f *fspb.File) { 201 | w.walkMu.Lock() 202 | w.walk.File = append(w.walk.File, f) 203 | w.walkMu.Unlock() 204 | } 205 | 206 | func (w *Walker) addNotificationToWalk(s fspb.Notification_Severity, path, msg string) { 207 | w.walkMu.Lock() 208 | w.walk.Notification = append(w.walk.Notification, &fspb.Notification{ 209 | Severity: s, 210 | Path: path, 211 | Message: msg, 212 | }) 213 | w.walkMu.Unlock() 214 | } 215 | 216 | // relDirDepth calculates the path depth relative to the origin. 217 | func (w *Walker) relDirDepth(origin, path string) uint32 { 218 | return uint32(len(strings.Split(path, string(filepath.Separator))) - len(strings.Split(origin, string(filepath.Separator)))) 219 | } 220 | 221 | // worker is a worker routine that reads paths from chPaths and walks all the files and 222 | // subdirectories until the channel is exhausted. All discovered files are converted to 223 | // File and processed with w.process(). 224 | func (w *Walker) worker(ctx context.Context, chPaths <-chan string) error { 225 | for path := range chPaths { 226 | baseInfo, err := os.Stat(path) 227 | if err != nil { 228 | return fmt.Errorf("unable to get file info for base path %q: %v", path, err) 229 | } 230 | baseDev, err := fsstat.DevNumber(baseInfo) 231 | if err != nil { 232 | return fmt.Errorf("unable to get file stat on base path %q: %v", path, err) 233 | } 234 | if err := filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error { 235 | // If the initial stat of the root dir fails we can get a nil value for d along with the 236 | // PathError from os.Stat. See WalkDirFunc for details. 237 | if d == nil && err != nil { 238 | msg := fmt.Sprintf("failed to stat root dir %q: %s", p, err) 239 | log.Print(msg) 240 | w.addNotificationToWalk(fspb.Notification_WARNING, p, msg) 241 | return err 242 | } 243 | // This catches the other error state of d != nil and err != nil, indicating 244 | // there was an error with the directory's ReadDir call. 245 | if err != nil { 246 | msg := fmt.Sprintf("failed to walk %q: %s", p, err) 247 | log.Print(msg) 248 | w.addNotificationToWalk(fspb.Notification_WARNING, p, msg) 249 | return nil // returning SkipDir on a file would skip the rest of the files in the dir 250 | } 251 | info, err := d.Info() 252 | if err != nil { 253 | msg := fmt.Sprintf("failed to get FileInfo for %q: %s", d.Name(), err) 254 | log.Print(msg) 255 | return nil 256 | } 257 | p = NormalizePath(p, info.IsDir()) 258 | 259 | // Checking various exclusions based on flags in the walker policy. 260 | if w.isExcluded(p) { 261 | if w.Verbose { 262 | w.addNotificationToWalk(fspb.Notification_INFO, p, fmt.Sprintf("skipping %q: excluded", p)) 263 | } 264 | if info.IsDir() { 265 | return filepath.SkipDir 266 | } 267 | return nil // returning SkipDir on a file would skip the rest of the files in the dir 268 | } 269 | if w.pol.IgnoreIrregularFiles && !info.Mode().IsRegular() && !info.IsDir() { 270 | if w.Verbose { 271 | w.addNotificationToWalk(fspb.Notification_INFO, p, fmt.Sprintf("skipping %q: irregular file (mode: %s)", p, info.Mode())) 272 | } 273 | return nil 274 | } 275 | f, err := w.convert(p, info) 276 | if err != nil { 277 | return err 278 | } 279 | if w.pol.MaxDirectoryDepth > 0 && info.IsDir() && w.relDirDepth(path, p) > w.pol.MaxDirectoryDepth { 280 | w.addNotificationToWalk(fspb.Notification_WARNING, p, fmt.Sprintf("skipping %q: more than %d into base path %q", p, w.pol.MaxDirectoryDepth, path)) 281 | return filepath.SkipDir 282 | } 283 | if !w.pol.WalkCrossDevice && f.Stat != nil && baseDev != f.Stat.Dev { 284 | msg := fmt.Sprintf("skipping %q: file is on different device", p) 285 | log.Printf(msg) 286 | if w.Verbose { 287 | w.addNotificationToWalk(fspb.Notification_INFO, p, msg) 288 | } 289 | if info.IsDir() { 290 | return filepath.SkipDir 291 | } 292 | return nil // returning SkipDir on a file would skip the rest of the files in the dir 293 | } 294 | 295 | return w.process(ctx, f) 296 | }); err != nil { 297 | return fmt.Errorf("error walking root include path %q: %v", path, err) 298 | } 299 | } 300 | return nil 301 | } 302 | 303 | // Run is the main function of Walker. It discovers all files under included paths 304 | // (minus excluded ones) and processes them. 305 | // This does NOT follow symlinks - fortunately we don't need it either. 306 | func (w *Walker) Run(ctx context.Context) error { 307 | walkID := uuid.New().String() 308 | hn, err := os.Hostname() 309 | if err != nil { 310 | return err 311 | } 312 | w.walk = &fspb.Walk{ 313 | Version: walkVersion, 314 | Id: walkID, 315 | Policy: w.pol, 316 | Hostname: hn, 317 | StartWalk: timestamppb.Now(), 318 | } 319 | 320 | chPaths := make(chan string, 10) 321 | var wg sync.WaitGroup 322 | var errs []string 323 | var errsMu sync.Mutex 324 | for i := 0; i < parallelism; i++ { 325 | wg.Add(1) 326 | go func() { 327 | defer wg.Done() 328 | if err := w.worker(ctx, chPaths); err != nil { 329 | errsMu.Lock() 330 | errs = append(errs, err.Error()) 331 | errsMu.Unlock() 332 | } 333 | }() 334 | } 335 | 336 | includes := map[string]bool{} 337 | for _, p := range w.pol.Include { 338 | p := filepath.Clean(p) 339 | if _, ok := includes[p]; ok { 340 | continue 341 | } 342 | includes[p] = true 343 | chPaths <- p 344 | } 345 | close(chPaths) 346 | wg.Wait() 347 | if len(errs) != 0 { 348 | return fmt.Errorf("unable to complete Walk:\n%s", strings.Join(errs, "\n")) 349 | } 350 | 351 | // Finishing work by writing out the report. 352 | w.walk.StopWalk = timestamppb.Now() 353 | if w.WalkCallback == nil { 354 | return nil 355 | } 356 | return w.WalkCallback(ctx, w.walk) 357 | } 358 | -------------------------------------------------------------------------------- /walker_darwin_test.go: -------------------------------------------------------------------------------- 1 | package fswalker 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | func setTimes(st syscall.Stat_t, a, m, c syscall.Timespec) syscall.Stat_t { 8 | st.Atimespec = a 9 | st.Mtimespec = m 10 | st.Ctimespec = c 11 | 12 | return st 13 | } 14 | -------------------------------------------------------------------------------- /walker_linux_test.go: -------------------------------------------------------------------------------- 1 | package fswalker 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | func setTimes(st syscall.Stat_t, a, m, c syscall.Timespec) syscall.Stat_t { 8 | st.Atim = a 9 | st.Mtim = m 10 | st.Ctim = c 11 | 12 | return st 13 | } 14 | -------------------------------------------------------------------------------- /walker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package fswalker 16 | 17 | import ( 18 | "context" 19 | "os" 20 | "path/filepath" 21 | "reflect" 22 | "sort" 23 | "syscall" 24 | "testing" 25 | "time" 26 | 27 | "github.com/google/go-cmp/cmp" 28 | "google.golang.org/protobuf/proto" 29 | "google.golang.org/protobuf/types/known/timestamppb" 30 | 31 | "github.com/google/fswalker/internal/metrics" 32 | fspb "github.com/google/fswalker/proto/fswalker" 33 | ) 34 | 35 | type outpathWriter string 36 | 37 | func (o outpathWriter) writeWalk(ctx context.Context, walk *fspb.Walk) error { 38 | walkBytes, err := proto.Marshal(walk) 39 | if err != nil { 40 | return err 41 | } 42 | return WriteFile(ctx, string(o), walkBytes, 0444) 43 | } 44 | 45 | // testFile implements the os.FileInfo interface. 46 | // For more details, see: https://golang.org/src/os/types.go?s=479:840#L11 47 | type testFile struct { 48 | name string 49 | size int64 50 | mode os.FileMode 51 | modTime time.Time 52 | isDir bool 53 | sys *syscall.Stat_t 54 | } 55 | 56 | func (t *testFile) Name() string { return t.name } 57 | func (t *testFile) Size() int64 { return t.size } 58 | func (t *testFile) Mode() os.FileMode { return t.mode } 59 | func (t *testFile) ModTime() time.Time { return t.modTime } 60 | func (t *testFile) IsDir() bool { return t.isDir } 61 | func (t *testFile) Sys() interface{} { return t.sys } 62 | 63 | func TestWalkerFromPolicyFile(t *testing.T) { 64 | path := filepath.Join(testdataDir, "defaultClientPolicy.asciipb") 65 | wantPol := &fspb.Policy{ 66 | Version: 1, 67 | MaxHashFileSize: 1048576, 68 | Include: []string{ 69 | "/", 70 | }, 71 | ExcludePfx: []string{ 72 | "/usr/src/linux-headers", 73 | "/usr/share/", 74 | "/proc/", 75 | "/sys/", 76 | "/tmp/", 77 | "/var/log/", 78 | "/var/tmp/", 79 | }, 80 | } 81 | 82 | ctx := context.Background() 83 | wlkr, err := WalkerFromPolicyFile(ctx, path) 84 | if err != nil { 85 | t.Errorf("WalkerFromPolicyFile() error: %v", err) 86 | return 87 | } 88 | diff := cmp.Diff(wlkr.pol, wantPol, cmp.Comparer(proto.Equal)) 89 | if diff != "" { 90 | t.Errorf("WalkerFromPolicyFile() policy: diff (-want +got):\n%s", diff) 91 | } 92 | } 93 | 94 | func TestProcess(t *testing.T) { 95 | ctx := context.Background() 96 | wlkr := &Walker{ 97 | walk: &fspb.Walk{}, 98 | } 99 | 100 | files := []*fspb.File{ 101 | {}, 102 | {}, 103 | {}, 104 | } 105 | for _, f := range files { 106 | if err := wlkr.process(ctx, f); err != nil { 107 | t.Errorf("process() error: %v", err) 108 | continue 109 | } 110 | } 111 | if diff := cmp.Diff(wlkr.walk.File, files, cmp.Comparer(proto.Equal)); diff != "" { 112 | t.Errorf("wlkr.walk.File != files: diff (-want +got):\n%s", diff) 113 | } 114 | } 115 | 116 | func TestIsExcluded(t *testing.T) { 117 | testCases := []struct { 118 | desc string 119 | excludes []string 120 | wantExcl bool 121 | }{ 122 | { 123 | desc: "test exclusion with empty list", 124 | excludes: []string{}, 125 | wantExcl: false, 126 | }, { 127 | desc: "test exclusion with entries but no match", 128 | excludes: []string{ 129 | "/tmp/", 130 | "/home/user2/", 131 | "/var/log/", 132 | }, 133 | wantExcl: false, 134 | }, { 135 | desc: "test exclusion with entries and exact match", 136 | excludes: []string{ 137 | "/tmp/", 138 | "/home/user/secret", 139 | "/var/log/", 140 | }, 141 | wantExcl: true, 142 | }, { 143 | desc: "test exclusion with entries and prefix match", 144 | excludes: []string{ 145 | "/tmp/", 146 | "/home/user", 147 | "/var/log/", 148 | }, 149 | wantExcl: true, 150 | }, 151 | } 152 | 153 | const path = "/home/user/secret" 154 | for _, tc := range testCases { 155 | wlkr := &Walker{ 156 | pol: &fspb.Policy{ 157 | ExcludePfx: tc.excludes, 158 | }, 159 | } 160 | 161 | gotExcl := wlkr.isExcluded(path) 162 | if gotExcl != tc.wantExcl { 163 | t.Errorf("isExcluded() %q = %v; want %v", tc.desc, gotExcl, tc.wantExcl) 164 | } 165 | } 166 | } 167 | 168 | func TestWantHashing(t *testing.T) { 169 | testCases := []struct { 170 | desc string 171 | hashpttrn []string 172 | wantHash bool 173 | }{ 174 | { 175 | desc: "test exclusion with empty list", 176 | hashpttrn: []string{}, 177 | wantHash: false, 178 | }, { 179 | desc: "test exclusion with entries but no match", 180 | hashpttrn: []string{ 181 | "/tmp/", 182 | "/home/user2/", 183 | "/var/log/", 184 | }, 185 | wantHash: false, 186 | }, { 187 | desc: "test exclusion with entries and exact match", 188 | hashpttrn: []string{ 189 | "/tmp/", 190 | "/home/user/secret", 191 | "/var/log/", 192 | }, 193 | wantHash: true, 194 | }, { 195 | desc: "test exclusion with entries and prefix match", 196 | hashpttrn: []string{ 197 | "/tmp/", 198 | "/home/user", 199 | "/var/log/", 200 | }, 201 | wantHash: true, 202 | }, 203 | } 204 | 205 | const path = "/home/user/secret" 206 | for _, tc := range testCases { 207 | wlkr := &Walker{ 208 | pol: &fspb.Policy{ 209 | HashPfx: tc.hashpttrn, 210 | }, 211 | } 212 | 213 | gotHash := wlkr.wantHashing(path) 214 | if gotHash != tc.wantHash { 215 | t.Errorf("wantHashing() %q = %v; want %v", tc.desc, gotHash, tc.wantHash) 216 | } 217 | } 218 | } 219 | 220 | func TestConvert(t *testing.T) { 221 | wlkr := &Walker{ 222 | pol: &fspb.Policy{ 223 | HashPfx: []string{ 224 | testdataDir, 225 | }, 226 | MaxHashFileSize: 1048576, 227 | }, 228 | } 229 | path := filepath.Join(testdataDir, "hashSumTest") 230 | st := syscall.Stat_t{ 231 | Dev: 1, 232 | Ino: 123456, 233 | Nlink: 2, 234 | Mode: 640, 235 | Uid: 123, 236 | Gid: 456, 237 | Rdev: 111, 238 | Size: 100, 239 | Blksize: 128, 240 | Blocks: 10, 241 | } 242 | atime := syscall.Timespec{Sec: time.Now().Unix(), Nsec: 100} 243 | mtime := syscall.Timespec{Sec: time.Now().Unix(), Nsec: 200} 244 | ctime := syscall.Timespec{Sec: time.Now().Unix(), Nsec: 300} 245 | st = setTimes(st, atime, mtime, ctime) 246 | 247 | info := &testFile{ 248 | name: "hashSumTest", 249 | size: 100, 250 | mode: os.FileMode(640), 251 | modTime: time.Now(), 252 | isDir: false, 253 | sys: &st, 254 | } 255 | 256 | mts := timestamppb.New(info.ModTime()) 257 | wantFile := &fspb.File{ 258 | Version: 1, 259 | Path: path, 260 | Info: &fspb.FileInfo{ 261 | Name: "hashSumTest", 262 | Size: 100, 263 | Mode: 640, 264 | Modified: mts, 265 | IsDir: false, 266 | }, 267 | Stat: &fspb.FileStat{ 268 | Dev: 1, 269 | Inode: 123456, 270 | Nlink: 2, 271 | Mode: 640, 272 | Uid: 123, 273 | Gid: 456, 274 | Rdev: 111, 275 | Size: 100, 276 | Blksize: 128, 277 | Blocks: 10, 278 | Atime: ×tamppb.Timestamp{Seconds: atime.Sec, Nanos: int32(atime.Nsec)}, 279 | Mtime: ×tamppb.Timestamp{Seconds: mtime.Sec, Nanos: int32(mtime.Nsec)}, 280 | Ctime: ×tamppb.Timestamp{Seconds: ctime.Sec, Nanos: int32(ctime.Nsec)}, 281 | }, 282 | Fingerprint: []*fspb.Fingerprint{ 283 | { 284 | Method: fspb.Fingerprint_SHA256, 285 | Value: "aeb02544df0ef515b21cab81ad5c0609b774f86879bf7e2e42c88efdaab2c75f", 286 | }, 287 | }, 288 | } 289 | 290 | gotFile, err := wlkr.convert(path, nil) // ensuring there is no problems with nil file stats. 291 | if err != nil { 292 | t.Fatal(err) 293 | } 294 | 295 | if wantFile.Path != gotFile.Path { 296 | t.Errorf("convert() path = %q; want: %q", gotFile.Path, wantFile.Path) 297 | } 298 | 299 | gotFile, err = wlkr.convert(path, info) 300 | if err != nil { 301 | t.Fatal(err) 302 | } 303 | 304 | diff := cmp.Diff(gotFile, wantFile, cmp.Comparer(proto.Equal)) 305 | if diff != "" { 306 | t.Errorf("convert() File proto: diff (-want +got):\n%s", diff) 307 | } 308 | } 309 | 310 | func TestRun(t *testing.T) { 311 | ctx := context.Background() 312 | tmpfile, err := os.CreateTemp("", "walk.pb") 313 | if err != nil { 314 | t.Fatal(err) 315 | } 316 | defer os.Remove(tmpfile.Name()) // clean up 317 | 318 | writer := outpathWriter(tmpfile.Name()) 319 | wlkr := &Walker{ 320 | pol: &fspb.Policy{ 321 | Include: []string{ 322 | testdataDir, 323 | }, 324 | HashPfx: []string{ 325 | testdataDir, 326 | }, 327 | MaxHashFileSize: 1048576, 328 | }, 329 | WalkCallback: writer.writeWalk, 330 | Counter: &metrics.Counter{}, 331 | } 332 | 333 | if err := wlkr.Run(ctx); err != nil { 334 | t.Errorf("Run() error: %v", err) 335 | return 336 | } 337 | 338 | wantMetrics := []string{ 339 | "dir-count", 340 | "file-size-sum", 341 | "file-count", 342 | "file-hash-count", 343 | } 344 | sort.Strings(wantMetrics) 345 | m := wlkr.Counter.Metrics() 346 | sort.Strings(m) 347 | if !reflect.DeepEqual(wantMetrics, m) { 348 | t.Errorf("wlkr.Counter.Metrics() = %q; want %q", m, wantMetrics) 349 | } 350 | for _, k := range m { 351 | if _, ok := wlkr.Counter.Get(k); !ok { 352 | t.Errorf("wlkr.Counter.Get(%q): not ok", k) 353 | } 354 | } 355 | 356 | b, err := ReadFile(ctx, tmpfile.Name()) 357 | if err != nil { 358 | t.Errorf("unable to read file %q: %v", tmpfile.Name(), err) 359 | } 360 | walk := &fspb.Walk{} 361 | if err := proto.Unmarshal(b, walk); err != nil { 362 | t.Errorf("unabled to decode proto file %q: %v", tmpfile.Name(), err) 363 | } 364 | st := walk.StartWalk.AsTime() 365 | et := walk.StopWalk.AsTime() 366 | if st.Before(time.Now().Add(-time.Hour)) || st.After(et) { 367 | t.Errorf("start time is not within bounds: %s < %s < %s", time.Now().Add(-time.Hour), st, et) 368 | } 369 | if et.Before(st) || et.After(time.Now()) { 370 | t.Errorf("stop time is not within bounds: %s < %s < %s", st, et, time.Now()) 371 | } 372 | if walk.Hostname == "" { 373 | t.Error("walk.Hostname is empty") 374 | } 375 | if walk.Id == "" { 376 | t.Error("walk.Id is empty") 377 | } 378 | } 379 | --------------------------------------------------------------------------------