Hacer queries a logs de CloudTrail usando Athena

Si estamos en la nube de AWS, en algún momento, alguien va a preguntar quién hizo qué y cuando. El por qué es más difícil de saber. ¿Y quién guarda quién hizo qué y cuando? Mr.CloudTrail.

CloudTrail

La herramienta estándar para registrar la actividad de nuestras cuentas es CloudTrail. CloudTrail puede registrar toda la actividad de nuestra y en última instancia, almacena los logs en S3.

El tema viene cuando queremos examinar eventos que sucedieron hace ya mucho tiempo. Si hace más de 90 días, no vamos a poder verlo en el Event history por AWS console.

AWS CloudTrail will only show the results of the CloudTrail Event History for the current region you are viewing for the last 90 days, and supports a range of AWS services. These events are limited to management events with create, modify, and delete API calls and account activity.

https://aws.amazon.com/es/cloudtrail/faqs/

Athena

¿Qué es Athena?

Amazon Athena is an interactive query service that makes it easy to analyze data in Amazon S3 using standard SQL. Athena is serverless, so there is no infrastructure to manage, and you pay only for the queries that you run.

https://aws.amazon.com/es/blogs/big-data/analyzing-data-in-s3-using-amazon-athena/

En resumidas cuentas, desde Athena haremos consultas SQL a nuestro bucket de S3 y buscaremos diversa información.

La tabla

Vamos a tener que crear la tabla de Athena. Lo más fácil es hacerlo desde la AWS Consola de CloudTrail, en Event History, cuando sale un icono de “Create Table in Athena”.

https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html

La DDL (Data Description Language) de la tabla sería algo así:

CREATE EXTERNAL TABLE `cloudtrail_logs`(
  `eventversion` string COMMENT 'from deserializer',
  `useridentity` struct<type:string,principalid:string,arn:string,accountid:string,invokedby:string,accesskeyid:string,username:string,sessioncontext:struct<attributes:struct<mfaauthenticated:string,creationdate:string>,sessionissuer:struct<type:string,principalid:string,arn:string,accountid:string,username:string>>> COMMENT 'from deserializer',
  `eventtime` string COMMENT 'from deserializer',
  `eventsource` string COMMENT 'from deserializer',
  `eventname` string COMMENT 'from deserializer',
  `awsregion` string COMMENT 'from deserializer',
  `sourceipaddress` string COMMENT 'from deserializer',
  `useragent` string COMMENT 'from deserializer',
  `errorcode` string COMMENT 'from deserializer',
  `errormessage` string COMMENT 'from deserializer',
  `requestparameters` string COMMENT 'from deserializer',
  `responseelements` string COMMENT 'from deserializer',
  `additionaleventdata` string COMMENT 'from deserializer',
  `requestid` string COMMENT 'from deserializer',
  `eventid` string COMMENT 'from deserializer',
  `resources` array<struct<arn:string,accountid:string,type:string>> COMMENT 'from deserializer',
  `eventtype` string COMMENT 'from deserializer',
  `apiversion` string COMMENT 'from deserializer',
  `readonly` string COMMENT 'from deserializer',
  `recipientaccountid` string COMMENT 'from deserializer',
  `serviceeventdetails` string COMMENT 'from deserializer',
  `sharedeventid` string COMMENT 'from deserializer',
  `vpcendpointid` string COMMENT 'from deserializer')
ROW FORMAT SERDE
  'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT
  'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://mybucket/AWSLogs/012345678901/CloudTrail/'
TBLPROPERTIES (
  'classification'='cloudtrail',
  'transient_lastDdlTime'='1572536550')

Con esta query podemos ir luego a Athena y crear la tabla.

Athena también nos pedirá que especifiquemos donde queremos guardar los resultados, para ello podemos ir a “Settings” en Athena y poner un S3 bucket y el path que prefiramos.

Ahora ya tenemos la tabla y podemos hacer queries!

Rendimiento de la tabla sin particionar vs particionado

Vamos a hacer una query para buscar eventos que han sucedido en un determinado tiempo.

SELECT * FROM "default"."cloudtrail_logs" 
WHERE eventsource = 'elasticloadbalancing.amazonaws.com'
AND eventtime > '2016-01-01T00:00:00Z'
AND eventtime < '2016-06-01T00:00:00Z'

Canceled
Time in queue:
0.234 sec
Run time:
5 min 3.985 sec
Data scanned:
199.44 GB

Paré la query porque llevaba ya 200 GB escaneado sin visos de acabar. Veamos como funciona cuando tenemos una tabla ya particionada.

SELECT * FROM "data_lake"."cloudtrail_logs" 
WHERE eventsource = 'elasticloadbalancing.amazonaws.com'
AND year = '2016' AND month < '06'

Completed
Time in queue:
0.204 sec
Run time:
33.286 sec
Data scanned:
498.85 MB

Al estar la tabla particionada podemos filtrar los datos usando esas columnas (year, month, day) y así nos evitamos un full search en toda la tabla (S3 bucket). Esto por un lado hace que tengamos datos más rápido y por otro que paguemos mucho menos.

Cómo particionar?

En el caso de CloudTrail, el path a los datos se particiona de este modo:

s3://mybucket/AWSLogs/012345678901/CloudTrail/$region/$year/$month/$day

En la DDL que hemos enseñado antes no hay particionamiento con lo que cualquier busqueda, ha de escanear casi toda la tabla y eso son muchos Gigabytes.

Entonces, modificaremos la DLL para generar una nueva tabla añadiendo el particionamiento. Bueno, la definición del particionamiento.

CREATE EXTERNAL TABLE `cloudtrail_logs`(
  `eventversion` string COMMENT 'from deserializer', 
  `useridentity` struct<type:string,principalid:string,arn:string,accountid:string,invokedby:string,accesskeyid:string,username:string,sessioncontext:struct<attributes:struct<mfaauthenticated:string,creationdate:string>,sessionissuer:struct<type:string,principalid:string,arn:string,accountid:string,username:string>>> COMMENT 'from deserializer', 
  `eventtime` string COMMENT 'from deserializer', 
  `eventsource` string COMMENT 'from deserializer', 
  `eventname` string COMMENT 'from deserializer', 
  `awsregion` string COMMENT 'from deserializer', 
  `sourceipaddress` string COMMENT 'from deserializer', 
  `useragent` string COMMENT 'from deserializer', 
  `errorcode` string COMMENT 'from deserializer', 
  `errormessage` string COMMENT 'from deserializer', 
  `requestparameters` string COMMENT 'from deserializer', 
  `responseelements` string COMMENT 'from deserializer', 
  `additionaleventdata` string COMMENT 'from deserializer', 
  `requestid` string COMMENT 'from deserializer', 
  `eventid` string COMMENT 'from deserializer', 
  `resources` array<struct<arn:string,accountid:string,type:string>> COMMENT 'from deserializer', 
  `eventtype` string COMMENT 'from deserializer', 
  `apiversion` string COMMENT 'from deserializer', 
  `readonly` string COMMENT 'from deserializer', 
  `recipientaccountid` string COMMENT 'from deserializer', 
  `serviceeventdetails` string COMMENT 'from deserializer', 
  `sharedeventid` string COMMENT 'from deserializer', 
  `vpcendpointid` string COMMENT 'from deserializer')
PARTITIONED BY ( 
  region string,
  `year` string, 
  `month` string, 
  `day` string)
ROW FORMAT SERDE 
  'com.amazon.emr.hive.serde.CloudTrailSerde' 
STORED AS INPUTFORMAT 
  'com.amazon.emr.cloudtrail.CloudTrailInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://mybucket/AWSLogs/012345678901/CloudTrail/'
TBLPROPERTIES (
  'classification'='cloudtrail', 
  'transient_lastDdlTime'='1637748939')

En este momento si hacemos un “Preview Table” no va a mostrar resultados porque como hemos especificado que nuestra tabla tiene particiones, hemos de añadirlas.

Lo ideal sería tener alguna Lambda que mediante eventos o de forma rutinaria añada la siguiente partición a la tabla conforme pasan los dias, años, etc.

Yo simplemente hice un script en Python para que me sacase las queries y luego lanzarlas a Athena via DataGrip. Vamos, que lo podéis hacer más elegante.

Añadir la partición

Un ejemplo de como añadir las particiones a las tablas sería así:

ALTER TABLE cloudtrail_logs
ADD PARTITION (year='2021', month='01', day='02', region='eu-west-1')
location  's3://mybucket/AWSLogs/012345678901/CloudTrail/eu-west-1/2021/01/02/';

Ejemplo de búsqueda

Vamos a buscar un domino dentro de nuestra hosted zone usando Athena.

SELECT useridentity,
       useragent,
       useridentity.username,
       useridentity.accountid,
       useridentity.sessioncontext.attributes.creationdate,
       json_extract(requestparameters,'$.changeBatch.changes[0].resourceRecordSet.name') AS domainName,
       json_extract(requestparameters,'$.changeBatch.changes[0].resourceRecordSet.type') AS recordType,
       json_extract(requestparameters,'$.changeBatch.changes[0].resourceRecordSet.resourceRecords') AS resourceRecords
FROM cloudtrail_logs_cloudtrail
WHERE (eventsource = 'route53.amazonaws.com'
      AND eventname = 'ChangeResourceRecordSets'
      AND json_extract_scalar(requestparameters,'$.changeBatch.changes[0].action') = 'CREATE'
      AND json_extract_scalar(requestparameters,'$.changeBatch.changes[0].resourceRecordSet.name') = 'www.mydomain.com.'
      AND year = '2020' AND region  = 'us-east-1' AND month = '06'
      )

Con esta query vamos a buscar todos los eventsources que correspondan a route53 donde el eventname sea ‘ChangeRecourceRecordSets’ y la acción sea ‘CREATE’. Buscamos de esta manera quien creo unos registros en una zona DNS sin especificar.

Leave a Reply

Your email address will not be published. Required fields are marked *