lib/auditlogsapi/filters.go (158 lines of code) (raw):

// Copyright 2020 Google LLC. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package auditlogsapi import ( "fmt" "regexp" "strings" "time" "google.golang.org/grpc/codes" /* copybara-comment */ "google.golang.org/grpc/status" /* copybara-comment */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/auditlog" /* copybara-comment: auditlog */ apb "github.com/GoogleCloudPlatform/healthcare-federated-access-services/proto/auditlogs/v0" /* copybara-comment: auditlogs_go_proto */ ) var ( // Warning: adding allow characters could lead to injection, need to double check valueEscape(). expRE = regexp.MustCompile(`(type|time|text|decision)\s*(=|>=|<=|:)\s*\"([\s\(\)\.\-\+,'#@;%:_/0-9A-Za-z]+)\"`) ) // extractFilters validates the filters and returns a Stackdriver Logging filter. // Currently supports a conjunction of time expressions. // time corresponds to to Stackdriver Logging field timestamp. // For guidance on filtering see: https://aip.dev/160 func extractFilters(in string) (string, error) { if in == "" { return "", nil } var exps []*exp for _, ss := range strings.Split(in, " AND ") { e, err := parseExp(ss) if err != nil { return "", err } exps = append(exps, e) } return toCELFilter(exps), nil } func parseExp(s string) (*exp, error) { s = strings.TrimSpace(s) matches := expRE.FindAllStringSubmatch(s, -1) if len(matches) == 0 { return nil, status.Errorf(codes.InvalidArgument, "unknown expression format") } ss := matches[0][1:] var field expField for _, f := range allowFields { if ss[0] == string(f) { field = f } } if len(field) == 0 { return nil, status.Errorf(codes.InvalidArgument, "unknown expression field: %s", ss[0]) } switch field { case fieldTime: // time allows >= and <= if ss[1] != string(gte) && ss[1] != string(lte) { return nil, status.Errorf(codes.InvalidArgument, "not allowed op for time field: %s", ss[1]) } case fieldType: // type allows = if ss[1] != string(equals) { return nil, status.Errorf(codes.InvalidArgument, "not allowed op for type field: %s", ss[1]) } case fieldText: // text allows : and = if ss[1] != string(equals) && ss[1] != string(contains) { return nil, status.Errorf(codes.InvalidArgument, "not allowed op for text field: %s", ss[1]) } case fieldDecision: // decision allows = if ss[1] != string(equals) { return nil, status.Errorf(codes.InvalidArgument, "not allowed op for decision field: %s", ss[1]) } default: return nil, status.Errorf(codes.Internal, "unknown expression field in op checker: %s", field) } op := expOp(ss[1]) value := strings.Trim(ss[2], `"`) if field == fieldTime { if _, err := time.Parse(time.RFC3339, value); err != nil { return nil, status.Errorf(codes.InvalidArgument, "time value not in RFC3339 format: %s", value) } } if field == fieldType { if value != apb.LogType_REQUEST.String() && value != apb.LogType_POLICY.String() { return nil, status.Errorf(codes.InvalidArgument, "type value not allowed: %s", value) } } if field == fieldDecision { value = strings.ToUpper(value) if value != apb.Decision_PASS.String() && value != apb.Decision_FAIL.String() { return nil, status.Errorf(codes.InvalidArgument, "decision value not allowed: %s", value) } } return &exp{field: field, op: op, value: value}, nil } // exp : expression support from request type exp struct { field expField op expOp value string } func (s *exp) toCELFilter() string { value := valueEscape(s.value) switch s.field { case fieldTime: return fmt.Sprintf(`timestamp %s "%s"`, s.op, value) case fieldType: return fmt.Sprintf(`labels.type = "%s"`, toAuditLogType(value)) case fieldText: return toTextFilter(s.op, value) case fieldDecision: return fmt.Sprintf(`labels.pass_auth_check = "%s"`, toDecisionValue(value)) default: return "" } } func toCELFilter(exps []*exp) string { var ss []string for _, e := range exps { ss = append(ss, e.toCELFilter()) } return strings.Join(ss, " AND ") } func toAuditLogType(ty string) string { switch ty { case apb.LogType_REQUEST.String(): return auditlog.TypeRequestLog case apb.LogType_POLICY.String(): return auditlog.TypePolicyLog default: return "" } } func toTextFilter(op expOp, value string) string { list := []string{ fmt.Sprintf(`textPayload %s "%s"`, op, value), } for _, l := range auditlog.SearchableFields { list = append(list, fmt.Sprintf(`%s %s "%s"`, l, op, value)) } s := strings.Join(list, " OR ") return "(" + s + ")" } func toDecisionValue(v string) string { switch v { case apb.Decision_PASS.String(): return "true" case apb.Decision_FAIL.String(): return "false" default: return "" } } // valueEscape removes double quotes to ensure no injection. func valueEscape(s string) string { s = strings.ReplaceAll(s, `"`, ``) return s } type expField string type expOp string var ( fieldTime expField = "time" fieldText expField = "text" fieldType expField = "type" fieldDecision expField = "decision" allowFields = []expField{fieldTime, fieldText, fieldType, fieldDecision} equals expOp = "=" contains expOp = ":" gte expOp = ">=" lte expOp = "<=" )