Skip to content

Commit b46ad3b

Browse files
fix(rules): flag stale cloudwatch log streams (#38)
1 parent 8573cb2 commit b46ad3b

8 files changed

Lines changed: 96 additions & 13 deletions

File tree

.changeset/metal-yaks-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cloudburn/rules': patch
3+
---
4+
5+
Flag CloudWatch unused log streams when they have never received events or when their last ingestion was more than 90 days ago.

docs/reference/rule-ids.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
5454

5555
`CLDBRN-AWS-EBS-1` flags previous-generation EBS volume types (`gp2`, `io1`, and `standard`) and does not flag current-generation HDD families such as `st1` or `sc1`.
5656

57-
`CLDBRN-AWS-CLOUDWATCH-2` flags only log streams with no observed event history. Streams with older events are not treated as unused by this rule.
57+
`CLDBRN-AWS-CLOUDWATCH-2` flags log streams with no observed event history and log streams whose `lastIngestionTime` is more than 90 days old. Delivery-managed log groups remain exempt.
5858

5959
`CLDBRN-AWS-EC2-9` flags only families with a curated Graviton-equivalent path. Instances without architecture metadata or outside the curated family set are skipped.
6060

packages/rules/src/aws/cloudwatch/unused-log-streams.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@ import { createFinding, createFindingMatch, createRule } from '../../shared/help
22

33
const RULE_ID = 'CLDBRN-AWS-CLOUDWATCH-2';
44
const RULE_SERVICE = 'cloudwatch';
5-
const RULE_MESSAGE = 'CloudWatch log streams that have never received events should be removed.';
5+
const RULE_MESSAGE =
6+
'CloudWatch log streams that have never received events or have been inactive for more than 90 days should be removed.';
7+
const DAY_MS = 24 * 60 * 60 * 1000;
8+
const UNUSED_LOG_STREAM_DAYS = 90;
69

710
const toLogGroupScopeKey = (region: string, accountId: string, logGroupName: string): string =>
811
`${region}:${accountId}:${logGroupName}`;
912

10-
/** Flag CloudWatch log streams that have never received events outside delivery-managed log groups. */
13+
/** Flag CloudWatch log streams with no event history or stale ingestion outside delivery-managed log groups. */
1114
export const cloudWatchUnusedLogStreamsRule = createRule({
1215
id: RULE_ID,
1316
name: 'CloudWatch Unused Log Streams',
14-
description: 'Flag CloudWatch log streams that have never received events outside delivery-managed log groups.',
17+
description:
18+
'Flag CloudWatch log streams that have never received events or whose last ingestion was more than 90 days ago outside delivery-managed log groups.',
1519
message: RULE_MESSAGE,
1620
provider: 'aws',
1721
service: RULE_SERVICE,
1822
supports: ['discovery'],
1923
discoveryDependencies: ['aws-cloudwatch-log-groups', 'aws-cloudwatch-log-streams'],
2024
evaluateLive: ({ resources }) => {
25+
const cutoff = Date.now() - UNUSED_LOG_STREAM_DAYS * DAY_MS;
2126
const logGroups = resources.get('aws-cloudwatch-log-groups');
2227
const knownLogGroups = new Set(
2328
logGroups.map((logGroup) => toLogGroupScopeKey(logGroup.region, logGroup.accountId, logGroup.logGroupName)),
@@ -36,9 +41,10 @@ export const cloudWatchUnusedLogStreamsRule = createRule({
3641
return (
3742
knownLogGroups.has(logGroupScopeKey) &&
3843
!deliveryManagedLogGroups.has(logGroupScopeKey) &&
39-
logStream.firstEventTimestamp === undefined &&
40-
logStream.lastEventTimestamp === undefined &&
41-
logStream.lastIngestionTime === undefined
44+
((logStream.firstEventTimestamp === undefined &&
45+
logStream.lastEventTimestamp === undefined &&
46+
logStream.lastIngestionTime === undefined) ||
47+
(logStream.lastIngestionTime !== undefined && logStream.lastIngestionTime < cutoff))
4248
);
4349
})
4450
.map((logStream) => createFindingMatch(logStream.arn, logStream.region, logStream.accountId));

packages/rules/test/cloudwatch-unused-log-streams.test.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { cloudWatchUnusedLogStreamsRule } from '../src/aws/cloudwatch/unused-log-streams.js';
33
import type { AwsCloudWatchLogGroup, AwsCloudWatchLogStream } from '../src/index.js';
44
import { LiveResourceBag } from '../src/index.js';
55

6+
const DAY_MS = 24 * 60 * 60 * 1000;
7+
68
const createLogGroup = (overrides: Partial<AwsCloudWatchLogGroup> = {}): AwsCloudWatchLogGroup => ({
79
accountId: '123456789012',
810
logGroupArn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app',
@@ -21,6 +23,15 @@ const createLogStream = (overrides: Partial<AwsCloudWatchLogStream> = {}): AwsCl
2123
});
2224

2325
describe('cloudWatchUnusedLogStreamsRule', () => {
26+
beforeEach(() => {
27+
vi.useFakeTimers();
28+
vi.setSystemTime(new Date('2026-03-19T00:00:00.000Z'));
29+
});
30+
31+
afterEach(() => {
32+
vi.useRealTimers();
33+
});
34+
2435
it('flags log streams with no event history', () => {
2536
const finding = cloudWatchUnusedLogStreamsRule.evaluateLive?.({
2637
catalog: {
@@ -38,7 +49,8 @@ describe('cloudWatchUnusedLogStreamsRule', () => {
3849
ruleId: 'CLDBRN-AWS-CLOUDWATCH-2',
3950
service: 'cloudwatch',
4051
source: 'discovery',
41-
message: 'CloudWatch log streams that have never received events should be removed.',
52+
message:
53+
'CloudWatch log streams that have never received events or have been inactive for more than 90 days should be removed.',
4254
findings: [
4355
{
4456
resourceId:
@@ -50,6 +62,28 @@ describe('cloudWatchUnusedLogStreamsRule', () => {
5062
});
5163
});
5264

65+
it('flags log streams whose last ingestion was more than 90 days ago', () => {
66+
const finding = cloudWatchUnusedLogStreamsRule.evaluateLive?.({
67+
catalog: {
68+
resources: [],
69+
searchRegion: 'us-east-1',
70+
indexType: 'LOCAL',
71+
},
72+
resources: new LiveResourceBag({
73+
'aws-cloudwatch-log-groups': [createLogGroup()],
74+
'aws-cloudwatch-log-streams': [createLogStream({ lastIngestionTime: Date.now() - 91 * DAY_MS })],
75+
}),
76+
});
77+
78+
expect(finding?.findings).toEqual([
79+
{
80+
resourceId: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app:log-stream:2026/03/16/[$LATEST]abc',
81+
region: 'us-east-1',
82+
accountId: '123456789012',
83+
},
84+
]);
85+
});
86+
5387
it('does not flag log streams with observed event history', () => {
5488
const finding = cloudWatchUnusedLogStreamsRule.evaluateLive?.({
5589
catalog: {
@@ -66,6 +100,38 @@ describe('cloudWatchUnusedLogStreamsRule', () => {
66100
expect(finding).toBeNull();
67101
});
68102

103+
it('does not flag log streams whose last ingestion was within 90 days', () => {
104+
const finding = cloudWatchUnusedLogStreamsRule.evaluateLive?.({
105+
catalog: {
106+
resources: [],
107+
searchRegion: 'us-east-1',
108+
indexType: 'LOCAL',
109+
},
110+
resources: new LiveResourceBag({
111+
'aws-cloudwatch-log-groups': [createLogGroup()],
112+
'aws-cloudwatch-log-streams': [createLogStream({ lastIngestionTime: Date.now() - 30 * DAY_MS })],
113+
}),
114+
});
115+
116+
expect(finding).toBeNull();
117+
});
118+
119+
it('does not flag log streams whose last ingestion was exactly 90 days ago', () => {
120+
const finding = cloudWatchUnusedLogStreamsRule.evaluateLive?.({
121+
catalog: {
122+
resources: [],
123+
searchRegion: 'us-east-1',
124+
indexType: 'LOCAL',
125+
},
126+
resources: new LiveResourceBag({
127+
'aws-cloudwatch-log-groups': [createLogGroup()],
128+
'aws-cloudwatch-log-streams': [createLogStream({ lastIngestionTime: Date.now() - 90 * DAY_MS })],
129+
}),
130+
});
131+
132+
expect(finding).toBeNull();
133+
});
134+
69135
it('does not flag streams inside delivery-managed log groups', () => {
70136
const finding = cloudWatchUnusedLogStreamsRule.evaluateLive?.({
71137
catalog: {

packages/rules/test/rule-metadata.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ describe('rule metadata', () => {
9494
expect(rule).toMatchObject({
9595
id: 'CLDBRN-AWS-CLOUDWATCH-2',
9696
name: 'CloudWatch Unused Log Streams',
97-
description: 'Flag CloudWatch log streams that have never received events outside delivery-managed log groups.',
98-
message: 'CloudWatch log streams that have never received events should be removed.',
97+
description:
98+
'Flag CloudWatch log streams that have never received events or whose last ingestion was more than 90 days ago outside delivery-managed log groups.',
99+
message:
100+
'CloudWatch log streams that have never received events or have been inactive for more than 90 days should be removed.',
99101
provider: 'aws',
100102
service: 'cloudwatch',
101103
supports: ['discovery'],

packages/sdk/test/exports.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ describe('sdk exports', () => {
6363
supports: ['discovery'],
6464
},
6565
{
66-
description: 'Flag CloudWatch log streams that have never received events outside delivery-managed log groups.',
66+
description:
67+
'Flag CloudWatch log streams that have never received events or whose last ingestion was more than 90 days ago outside delivery-managed log groups.',
6768
id: 'CLDBRN-AWS-CLOUDWATCH-2',
6869
provider: 'aws',
6970
service: 'cloudwatch',

packages/sdk/test/providers/aws-cloudwatch-logs-resource.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ describe('hydrateAwsCloudWatchLogStreams', () => {
213213
logStreams: [
214214
{
215215
arn: `arn:aws:logs:us-east-1:123456789012:log-group:${input.logGroupName}:log-stream:2026/03/16/[$LATEST]abc`,
216+
lastIngestionTime: 1_710_000_000_000,
216217
logStreamName: '2026/03/16/[$LATEST]abc',
217218
},
218219
],
@@ -238,7 +239,7 @@ describe('hydrateAwsCloudWatchLogStreams', () => {
238239
creationTime: undefined,
239240
firstEventTimestamp: undefined,
240241
lastEventTimestamp: undefined,
241-
lastIngestionTime: undefined,
242+
lastIngestionTime: 1_710_000_000_000,
242243
logGroupName: '/aws/lambda/app',
243244
logStreamName: '2026/03/16/[$LATEST]abc',
244245
region: 'us-east-1',

packages/sdk/test/providers/aws-discovery.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,7 @@ describe('discoverAwsResources', () => {
910910
{
911911
accountId: '123456789012',
912912
arn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app:log-stream:2026/03/16/[$LATEST]abc',
913+
lastIngestionTime: 1_710_000_000_000,
913914
logGroupName: '/aws/lambda/app',
914915
logStreamName: '2026/03/16/[$LATEST]abc',
915916
region: 'us-east-1',
@@ -945,6 +946,7 @@ describe('discoverAwsResources', () => {
945946
{
946947
accountId: '123456789012',
947948
arn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app:log-stream:2026/03/16/[$LATEST]abc',
949+
lastIngestionTime: 1_710_000_000_000,
948950
logGroupName: '/aws/lambda/app',
949951
logStreamName: '2026/03/16/[$LATEST]abc',
950952
region: 'us-east-1',

0 commit comments

Comments
 (0)