├── .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 | [](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 |
--------------------------------------------------------------------------------