github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/events/events.go (about)

     1  /*
     2  Copyright 2022 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package events
    18  
    19  import (
    20  	"crypto/sha256"
    21  	"encoding/hex"
    22  	"encoding/json"
    23  
    24  	"github.com/gravitational/trace"
    25  	"google.golang.org/protobuf/types/known/structpb"
    26  	"google.golang.org/protobuf/types/known/timestamppb"
    27  
    28  	auditlogpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/auditlog/v1"
    29  	"github.com/gravitational/teleport/api/utils"
    30  )
    31  
    32  func trimN(s string, n int) string {
    33  	// Starting at 2 to leave room for quotes at the begging and end.
    34  	charCount := 2
    35  	for i, r := range s {
    36  		// Make sure we always have room to add an escape character if necessary.
    37  		if charCount+1 > n {
    38  			return s[:i]
    39  		}
    40  		if r == rune('"') || r == '\\' {
    41  			charCount++
    42  		}
    43  		charCount++
    44  	}
    45  	return s
    46  }
    47  
    48  func maxSizePerField(maxLength, customFields int) int {
    49  	if customFields == 0 {
    50  		return maxLength
    51  	}
    52  	return maxLength / customFields
    53  }
    54  
    55  // TrimToMaxSize trims the DatabaseSessionQuery message content. The maxSize is used to calculate
    56  // per-field max size where only user input message fields DatabaseQuery and DatabaseQueryParameters are taken into
    57  // account.
    58  func (m *DatabaseSessionQuery) TrimToMaxSize(maxSize int) AuditEvent {
    59  	size := m.Size()
    60  	if size <= maxSize {
    61  		return m
    62  	}
    63  
    64  	out := utils.CloneProtoMsg(m)
    65  	out.DatabaseQuery = ""
    66  	out.DatabaseQueryParameters = nil
    67  
    68  	// Use 10% max size ballast + message size without custom fields.
    69  	sizeBallast := maxSize/10 + out.Size()
    70  	maxSize -= sizeBallast
    71  
    72  	// Check how many custom fields are set.
    73  	customFieldsCount := 0
    74  	if m.DatabaseQuery != "" {
    75  		customFieldsCount++
    76  	}
    77  	for range m.DatabaseQueryParameters {
    78  		customFieldsCount++
    79  	}
    80  
    81  	maxFieldsSize := maxSizePerField(maxSize, customFieldsCount)
    82  
    83  	out.DatabaseQuery = trimN(m.DatabaseQuery, maxFieldsSize)
    84  	if m.DatabaseQueryParameters != nil {
    85  		out.DatabaseQueryParameters = make([]string, len(m.DatabaseQueryParameters))
    86  	}
    87  	for i, v := range m.DatabaseQueryParameters {
    88  		out.DatabaseQueryParameters[i] = trimN(v, maxFieldsSize)
    89  	}
    90  	return out
    91  }
    92  
    93  // TrimToMaxSize trims the SessionStart event to the given maximum size.
    94  // Currently assumes that the largest field will be InitialCommand and tries to
    95  // trim that.
    96  func (e *SessionStart) TrimToMaxSize(maxSize int) AuditEvent {
    97  	size := e.Size()
    98  	if size <= maxSize {
    99  		return e
   100  	}
   101  
   102  	out := utils.CloneProtoMsg(e)
   103  	out.InitialCommand = nil
   104  
   105  	// Use 10% max size ballast + message size without InitialCommand
   106  	sizeBallast := maxSize/10 + out.Size()
   107  	maxSize -= sizeBallast
   108  
   109  	maxFieldSize := maxSizePerField(maxSize, len(e.InitialCommand))
   110  
   111  	out.InitialCommand = make([]string, len(e.InitialCommand))
   112  	for i, c := range e.InitialCommand {
   113  		out.InitialCommand[i] = trimN(c, maxFieldSize)
   114  	}
   115  
   116  	return out
   117  }
   118  
   119  // TrimToMaxSize trims the Exec event to the given maximum size.
   120  // Currently assumes that the largest field will be Command and tries to trim
   121  // that.
   122  func (e *Exec) TrimToMaxSize(maxSize int) AuditEvent {
   123  	size := e.Size()
   124  	if size <= maxSize {
   125  		return e
   126  	}
   127  
   128  	out := utils.CloneProtoMsg(e)
   129  	out.Command = ""
   130  
   131  	// Use 10% max size ballast + message size without Command
   132  	sizeBallast := maxSize/10 + out.Size()
   133  	maxSize -= sizeBallast
   134  
   135  	out.Command = trimN(e.Command, maxSize)
   136  
   137  	return out
   138  }
   139  
   140  // TrimToMaxSize trims the UserLogin event to the given maximum size.
   141  // The initial implementation is to cover concerns that a malicious user could
   142  // craft a request that creates error messages too large to be handled by the
   143  // underlying storage and thus cause the events to be omitted entirely. See
   144  // teleport-private#172.
   145  func (e *UserLogin) TrimToMaxSize(maxSize int) AuditEvent {
   146  	size := e.Size()
   147  	if size <= maxSize {
   148  		return e
   149  	}
   150  
   151  	out := utils.CloneProtoMsg(e)
   152  	out.Status.Error = ""
   153  	out.Status.UserMessage = ""
   154  
   155  	// Use 10% max size ballast + message size without Error and UserMessage
   156  	sizeBallast := maxSize/10 + out.Size()
   157  	maxSize -= sizeBallast
   158  
   159  	maxFieldSize := maxSizePerField(maxSize, 2)
   160  
   161  	out.Status.Error = trimN(e.Status.Error, maxFieldSize)
   162  	out.Status.UserMessage = trimN(e.Status.UserMessage, maxFieldSize)
   163  
   164  	return out
   165  }
   166  
   167  // ToUnstructured converts the event stored in the AuditEvent interface
   168  // to unstructured.
   169  // If the event is a session print event, it is converted to a plugins printEvent struct
   170  // which is then converted to structpb.Struct. Otherwise the event is marshaled directly.
   171  func ToUnstructured(evt AuditEvent) (*auditlogpb.EventUnstructured, error) {
   172  	payload, err := json.Marshal(evt)
   173  	if err != nil {
   174  		return nil, trace.Wrap(err)
   175  	}
   176  	id := computeEventID(evt, payload)
   177  	if err != nil {
   178  		return nil, trace.Wrap(err)
   179  	}
   180  
   181  	str := &structpb.Struct{}
   182  	if err := str.UnmarshalJSON(payload); err != nil {
   183  		return nil, trace.Wrap(err)
   184  	}
   185  
   186  	// If the event is a session print event, convert it to a printEvent struct
   187  	// to include the `data` field in the JSON.
   188  	if p, ok := evt.(*SessionPrint); ok {
   189  		const printEventDataKey = "data"
   190  		// append the `data` field to the unstructured event
   191  		str.Fields[printEventDataKey], err = structpb.NewValue(p.Data)
   192  		if err != nil {
   193  			return nil, trace.Wrap(err)
   194  		}
   195  	}
   196  
   197  	return &auditlogpb.EventUnstructured{
   198  		Type:         evt.GetType(),
   199  		Index:        evt.GetIndex(),
   200  		Time:         timestamppb.New(evt.GetTime()),
   201  		Id:           id,
   202  		Unstructured: str,
   203  	}, nil
   204  }
   205  
   206  // computeEventID computes the ID of the event. If the event already has an ID, it is returned.
   207  // Otherwise, the event is marshaled to JSON and the SHA256 hash of the JSON is returned.
   208  func computeEventID(evt AuditEvent, payload []byte) string {
   209  	id := evt.GetID()
   210  	if id != "" {
   211  		return id
   212  	}
   213  
   214  	hash := sha256.Sum256(payload)
   215  	return hex.EncodeToString(hash[:])
   216  }