This project doesn't work currently, it was moved out of another repo without being tested. I'm mainly publishing it because it has some notes and code that I wanted to keep around.
The goal of the project was to have a command line utility for viewing enriched cloudtrail sessions live. So essentially you can start the command, it will start trailing live events from cloudtrail, and the output will be normalized based on associated sessions.
In theory you could start with a given identity, get output limited to their current sessions and any future sessions (assume role, get session token, etc), proxied sessions (web console), sessions across access key collisions, or whatever other wierd stuff aws does with cloudtrail logs...
But again this doesn't really work, I'm just keeping the notes here now...
-
Handle out of order cross-account assume role calls
- CloudTrail doesn't gurantee event order, so a call using a session can come before the call to create the session itself
- The source and target events happen the same second, they can't be sorted on date.
- Maybe just need to have fall back ids?
-
Handle representing collisions in the outputted JSON.
- It is possible for two different sessions to have all the same identifying attributes we use to differentiate sessions. This happens primarily because access key id get's recycled occasionally and AWS doesn't output session tokens to the logs. See hunters-research-is-aws-recycling-your-access-key for more info.
- This kinda sucks.. but should be able to handle this case. For example to matter to us the collision has to be:
- Two sessions from the same source principal, otherwise we can tell them apart.
- Has to happen the same second, otherwise we can tell them apart (based on .userIdentity.sessionContext.attributes.creationDate).
- some other stuff maybe? i don't remember now..
- In any case, the result will be (in the context of this tool) that we know there is a collision and events by the same session that happen after
can be one of two principals from the same source session. i.e. at some point the internal tracked session identity keys (sessid key) for two seperate sessions,
both previously associated one-to-one with normal api sessions will become two sessid key's associated with two api sessions.
- We'd likely want to have a warning message + some visual indicator to indicate the confidence of the session tracking is downgraded (this will come up in other cases as well) and start tracking on useragent + ip or whatever.
-
Look into how various AWS managed assets create sessions
-
Some like the console, or CloudFormation will create a new key for each request without actually making any session modifying calls iirc
-
I can't remember how much of an issue this is, likely makes sense to look into what sessionContext looks like for these calls.
-
Add testing for logs directory
- SwitchRole and ConsoleLogin
- Events are not sent through event bridge because they are marked as read only events.
- AssumeRole can be readonly as well in some cases, I forget when/why though
- To link to a previous session we only have the role session arn and creation date, if these collide:
- We won't know which original user made the SwitchRole/ConsoleLogin call.
- Unless we want to differentiate based on source ip or user agent.
- Events are not sent through event bridge because they are marked as read only events.
- Include original + chain of event ids in outputted json?
Some stuff found in CloudTrail event type notes, something got messed up in this directory though and some of the files are duplicates currently.
iirc these files are supposed to track different identity type session changes. Both principal, entity, and assume role reduced cloudtrail logs to a common session key (a string) that could be referenced across all events which should corelate more or less to the principal or entity(?) as well as tracked edges between different principal/entity session keys as a result of a specific event log (assumerole, etc..).
How ctail works basically comes down to these two snippets.
For events that result in a session change we normalize some stuff to an intermidiate object that contains the stuff used to identify future sessions:
switch e.EventName {
case "ConsoleLogin":
//
// The ARN issuer will stay the same when ConsoleLogin is called from a role. See the role-console-login.json
// file for an example.
//
// This likely doesn't cover other cases of ConsoleLogin.
//
id.Arn = e.UserIdentity.SessionContext.SessionIssuer.Arn
case "AssumeRole":
//
// TODO: Check sharedEventID
//
id.Type = "AssumedRole"
id.Arn = e.ResponseElements.AssumedRoleUser.Arn
id.AccessKeyId = e.ResponseElements.Credentials.AccessKeyId
//
// Skip cross-account assume role events for now if this event isn't from the source account. If the
// target account event Identity happens to be returned first it causes subsequent sessions to have the
// original identity of the source account rather than the source user in the source account.
//
// TODO: This breaks when we don't have access to the source account.
//
if e.UserIdentity.AccountId != e.RecipientAccountId {
return nil
}
case "GetFederationToken":
id.Type = "FederatedUser"
id.Arn = e.ResponseElements.FederatedUser.Arn
id.AccessKeyId = e.ResponseElements.Credentials.AccessKeyId
}
Both the above internal representation, and future events can then be reduced to a session ID key:
switch i.Type {
case "IAMUser":
// IAMUser::12346789012:AIDAXXXXXXXXXXXXXXXXX:me
id = fmt.Sprintf("%s:%s:%s:%s:%s", i.Type, i.InvokedBy, i.AccountId, resourceId, i.UserName)
case "AssumedRole":
// AssumedRole::arn:aws:sts::123456789012:assumed-role/test2/XX:ASIAXXXXXXXXXXXXXXXX:1664688868
id = fmt.Sprintf("%s:%s:%s:%s:%d", i.Type, i.InvokedBy, i.Arn, i.AccessKeyId, i.SessionContext.Attributes.CreationDate.Unix())
//
// TODO: Cover ConsoleLogin from assume-role source. This is difficult because we can only identify the
// resulting session based on the original session arn and the creation time, there is no access
// key in there response. We also can't identify if the source should be a ConsoleLogin call.
//
case "AWSAccount":
// To actually reliably track sessions across accounts we need to use the sharedEventId.
//
// Can be in either of the following formats:
// AWSAccount::123456789012:AIDAXXXXXXXXXXXXXXXXX
// AWSAccount::123456789012:AIDAXXXXXXXXXXXXXXXXX:botocore-session-1661553185
id = fmt.Sprintf("%s:%s:%s", i.Type, i.AccountId, i.PrincipalId)
case "AWSService":
// Consolidate sessions that originate from AWS Services, they create too many unique sessions.
//
// AWSService:codepipeline.amazonaws.com
id = fmt.Sprintf("%s:%s", i.Type, i.InvokedBy)
case "FederatedUser":
// TODO: Ensure Source can match this
id = fmt.Sprintf("%s:%s:%s:%s:%d", i.Type, i.InvokedBy, i.Arn, i.AccessKeyId, i.SessionContext.Attributes.CreationDate.Unix())
default:
panic("unknown user identity type")
// AwsConsoleSignIn event type will not have a SessionContext
With the original session key and target key a session stream will look like:
Event1.Id(): AWSAccount::<acct-id>:AIDA...
Event1.Target(): AWSAccount::<acct-id>:AIDA... -> AssumedRole::<arn>:ASIA...:<unix timestamp>
Event2.Id(): AssumedRole::<arn>:ASIA...:<unix timestamp>
SessionContext is null when .userIdentity.type is
* Role
* SAMLUser
* WebIdentityUser
* AWSService
* null (`AwsServiceEvent` event type)
and sometimes when .userIdentity.type is (TODO: why?)
* IAMUser
// SessionIssuer provides information about how the credentials were obtained if the request was made with temporary
// security credentials.
//
// https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html#sessionissuer
//
type SessionIssuer struct {
// Type can be
// * Root
// * IAMUser
// * Role
Type string `json:"type"`
// PrincipalId the internal ID of the entity that was used to get credentials.
PrincipalId string `json:"principalId"`
// Arn the source (account, IAM user, or role) that was used to get temporary security credentials
Arn string `json:"arn"`
// AccountId is the account that owns the entity that was used to get credentials.
AccountId string `json:"accountId"`
// UserName
//
// The friendly name of the user or role that issued the session. The value that appears depends on the
// sessionIssuer identity type.
//
// * Root w/no alias: nil
// * Root w/alias: Account alias
// * IAMUser: Name of user
// * Role: Name of role
UserName string `json:"userName"`
}
This should correspond to the userIdentity key
// UserIdentity
//
// This will exist on all events, how it's used varies however depending on the context.
//
// More info: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html
//
type UserIdentity struct {
// Type can be:
//
// Same Account
// Persistent Access
// * Root
// * The request was made with your AWS account credentials. If the userIdentity type is Root and you set an
// alias for your account, the userName field contains your account alias.
// * IAMUser
// * The request was made with the credentials of an IAM user.
// * Role
// * The request was made with a persistent IAM identity that has specific permissions. The issuer of role
// sessions is always the role.
//
// Session Based Access
// * AssumedRole
// * The request was made with temporary security credentials that were obtained with a role by making a call
// to the AWS Security Token Service (AWS STS) AssumeRole API. This can include roles for Amazon EC2 and
// cross-account API access.
// * FederatedUser
// * The request was made with temporary security credentials that were obtained via a call to the AWS STS
// GetFederationToken API.
//
//
// Cross-Account Access
//
// AWSAccount and AWSService appear for type in your logs when there is cross-account access using an IAM role
// that you own.
//
// * AWSAccount
// * The request was made by another AWS account.
// * AWSService
// * The request was made by an AWS account that belongs to an AWS service.
//
//
// Other
// * Directory
// * The request was made to a directory service, and the type is unknown. Directory services include the
// following: Amazon WorkDocs and Amazon QuickSight.
// * Unknown
// * The request was made with an identity type that CloudTrail cannot determine.
//
//
// Undocumented types:
// * SAMLUser
// * WebIdentityUser
// * WebIdentityUser
// * null
// * Appears to only occur on `AwsServiceEvent` event types.
//
Type string `json:"type,omitempty"`
InvokedBy string `json:"invokedBy,omitempty"`
// PrincipalId
// A unique identifier for the entity that made the call. For requests made with temporary security credentials,
// this value includes the session name that is passed to the AssumeRole, AssumeRoleWithWebIdentity, or
// GetFederationToken API call.
//
// Optional: True
//
// Terms from https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html#intro-structure-terms
// * IAM Entities
// * An entity is the IAM resource objects that AWS uses for authentication. These include IAM users and roles.
//
// * Principals
// * A person or application that uses the AWS account root user, an IAM user, or an IAM role to sign in and
// make requests to AWS. Principals include federated users and assumed roles.
//
// Examples:
// AROAXXXXXXXXXXXXXXXXX:XX
// IAMUser: AIDAXXXXXXXXXXXXXXXXX
// AWSService: null
PrincipalId string `json:"principalId,omitempty"`
// The Amazon Resource Name (ARN) of the principal that made the call. The last section of the arn contains the
// user or role that made the call.
//
// Optional: True
//
Arn string `json:"arn,omitempty"`
UserName string `json:"UserName,omitempty"`
// AccountId The account that owns the entity that was used to get credentials.
AccountId string `json:"accountId,omitempty"`
AccessKeyId string `json:"accessKeyId,omitempty"`
SessionContext *awsapicallviacloudtrail.SessionContext `json:"sessionContext,omitempty"`
}
// EventType can be:
// * AwsApiCall
// * AwsConsoleSignIn
// * Attempts to sign in to the AWS Management Console, the AWS Discussion Forums, and the AWS Support Center.
// * https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-aws-console-sign-in-events.html
// * AwsServiceEvent
// * These events are created by AWS services but are not directly triggered by a request to a public AWS API.
// * https://docs.aws.amazon.com/awscloudtrail/latest/userguide/non-api-aws-service-events.html
EventType string `json:"eventType,omitempty"`
type AwsServiceIdentity struct {
BaseUserIdentity
InvokedBy string `json:"invokedBy"`
}
type AwsAccountIdentity struct {
BaseUserIdentity
// PrincipalId is
// * AIDA[A-Z2-7]{16} representing an IAM User.
// * AROA[A-Z2-7]{16}:<sessionName> representing a role session.
PrincipalId string `json:"principalId"`
AccountId string `json:"accountId"`
// Optional InvokedBy field so far only seen this set to "AWS Internal"
InvokedBy *string `json:"invokedBy"`
}
type RootIdentity struct {
BaseUserIdentity
// PrincipalId appears to be the account ID (or alias?)
PrincipalId string `json:"principalId"`
// UserName is the account alias if it is seet.
UserName *string `json:"userName"`
// Arn is the root ARN (arn:aws:iam::111111111111:root)
Arn string `json:"arn"`
AccountId string `json:"accountId"`
// AccessKeyId appears to be a temporary token associated with the current account in some way.
// * Using https://go.dev/play/p/-VgXwYUfRUC results in the current account ID.
// * Haven't seen the same ID occur twice in the logs yet, so unclear atm how to associate this identity.
// * It appears ASIA... ids relate to AROA... ids in some predictable way, may be able to use that.
AccessKeyId string `json:"accessKeyId"`
// SessionContext so far seems empty except the attributes object
SessionContext struct {
SessionIssuer SessionIssuer `json:"sessionIssuer"`
WebIdFederationData struct {
} `json:"webIdFederationData"`
Attributes struct {
CreationDate time.Time `json:"creationDate"`
MfaAuthenticated string `json:"mfaAuthenticated"`
} `json:"attributes"`
} `json:"sessionContext"`
}
type UserIdentity struct {
BaseUserIdentity
PrincipalId string `json:"principalId"`
Arn string `json:"arn"`
AccountId string `json:"accountId"`
AccessKeyId string `json:"accessKeyId"`
UserName string `json:"userName"`
SessionContext struct {
SessionIssuer struct {
} `json:"sessionIssuer"`
WebIdFederationData struct {
} `json:"webIdFederationData"`
Attributes struct {
CreationDate time.Time `json:"creationDate"`
MfaAuthenticated string `json:"mfaAuthenticated"`
} `json:"attributes"`
} `json:"sessionContext"`
InvokedBy string `json:"invokedBy"`
}
type AssumedRoleIdentity struct {
BaseUserIdentity
PrincipalId string `json:"principalId"`
Arn string `json:"arn"`
AccountId string `json:"accountId"`
SessionContext struct {
SessionIssuer struct {
Type string `json:"type"`
PrincipalId string `json:"principalId"`
Arn string `json:"arn"`
AccountId string `json:"accountId"`
UserName string `json:"userName"`
} `json:"sessionIssuer"`
WebIdFederationData struct {
} `json:"webIdFederationData"`
Attributes struct {
CreationDate time.Time `json:"creationDate"`
MfaAuthenticated string `json:"mfaAuthenticated"`
} `json:"attributes"`
} `json:"sessionContext"`
}