Skip to content

Commit 30f16ce

Browse files
committed
fix(tencent): fix events dump handling
1 parent 9d470ef commit 30f16ce

3 files changed

Lines changed: 160 additions & 58 deletions

File tree

pkg/providers/tencent/api/types_cloudaudit.go

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package api
22

3-
import "context"
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
)
49

510
const cloudAuditVersion = "2019-03-19"
611

712
type LookUpEventsRequest struct {
8-
StartTime *int64 `json:"StartTime,omitempty"`
9-
EndTime *int64 `json:"EndTime,omitempty"`
10-
MaxResults *uint64 `json:"MaxResults,omitempty"`
11-
NextToken *string `json:"NextToken,omitempty"`
12-
LookupAttributes []LookupAttribute `json:"LookupAttributes,omitempty"`
13+
StartTime *int64 `json:"StartTime,omitempty"`
14+
EndTime *int64 `json:"EndTime,omitempty"`
15+
MaxResults *int64 `json:"MaxResults,omitempty"`
16+
NextToken *string `json:"NextToken,omitempty"`
17+
LookupAttributes []LookupAttribute `json:"LookupAttributes,omitempty"`
1318
}
1419

1520
type LookupAttribute struct {
@@ -19,13 +24,42 @@ type LookupAttribute struct {
1924

2025
type LookUpEventsResponse struct {
2126
Response struct {
22-
NextToken *string `json:"NextToken"`
23-
ListOver *bool `json:"ListOver"`
24-
Events []CloudAuditEvent `json:"Events"`
25-
RequestID string `json:"RequestId"`
27+
NextToken *CloudAuditNextToken `json:"NextToken"`
28+
ListOver *bool `json:"ListOver"`
29+
Events []CloudAuditEvent `json:"Events"`
30+
RequestID string `json:"RequestId"`
2631
} `json:"Response"`
2732
}
2833

34+
type CloudAuditNextToken string
35+
36+
func (t *CloudAuditNextToken) UnmarshalJSON(data []byte) error {
37+
data = bytes.TrimSpace(data)
38+
if bytes.Equal(data, []byte("null")) {
39+
return nil
40+
}
41+
var s string
42+
if err := json.Unmarshal(data, &s); err == nil {
43+
*t = CloudAuditNextToken(s)
44+
return nil
45+
}
46+
var n json.Number
47+
decoder := json.NewDecoder(bytes.NewReader(data))
48+
decoder.UseNumber()
49+
if err := decoder.Decode(&n); err == nil {
50+
*t = CloudAuditNextToken(n.String())
51+
return nil
52+
}
53+
return fmt.Errorf("invalid CloudAudit NextToken %s", string(data))
54+
}
55+
56+
func (t *CloudAuditNextToken) String() string {
57+
if t == nil {
58+
return ""
59+
}
60+
return string(*t)
61+
}
62+
2963
type CloudAuditEvent struct {
3064
EventID *string `json:"EventId"`
3165
EventName *string `json:"EventName"`
@@ -46,7 +80,7 @@ type CloudAuditEvent struct {
4680
// recent operations so a CSPM detection can be cross-referenced. StartTime /
4781
// EndTime are unix seconds; pass 0 to leave them unset and fall back to the
4882
// CloudAudit default lookback window.
49-
func (c *Client) LookUpEvents(ctx context.Context, region string, startTime, endTime int64, maxResults uint64, nextToken string) (LookUpEventsResponse, error) {
83+
func (c *Client) LookUpEvents(ctx context.Context, region string, startTime, endTime int64, maxResults int64, nextToken, accessKeyID string) (LookUpEventsResponse, error) {
5084
req := LookUpEventsRequest{}
5185
if startTime > 0 {
5286
ts := startTime
@@ -57,11 +91,19 @@ func (c *Client) LookUpEvents(ctx context.Context, region string, startTime, end
5791
req.EndTime = &te
5892
}
5993
if maxResults > 0 {
60-
req.MaxResults = uint64Ptr(maxResults)
94+
req.MaxResults = int64Ptr(maxResults)
6195
}
6296
if nt := nextToken; nt != "" {
6397
req.NextToken = &nt
6498
}
99+
if accessKeyID != "" {
100+
req.LookupAttributes = []LookupAttribute{
101+
{
102+
AttributeKey: stringPtr("AccessKeyId"),
103+
AttributeValue: stringPtr(accessKeyID),
104+
},
105+
}
106+
}
65107
var resp LookUpEventsResponse
66108
err := c.DoJSON(ctx, "cloudaudit", cloudAuditVersion, "LookUpEvents", region, req, &resp)
67109
return resp, err

pkg/providers/tencent/cloudaudit/cloudaudit.go

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"strconv"
88
"strings"
9+
"time"
910

1011
"github.com/404tk/cloudtoolkit/pkg/providers/tencent/api"
1112
"github.com/404tk/cloudtoolkit/pkg/providers/tencent/auth"
@@ -18,6 +19,7 @@ import (
1819
type Driver struct {
1920
Credential auth.Credential
2021
clientOptions []api.Option
22+
Clock func() time.Time
2123
}
2224

2325
func (d *Driver) SetClientOptions(opts ...api.Option) {
@@ -30,46 +32,59 @@ func (d *Driver) newClient() *api.Client {
3032

3133
const (
3234
defaultLookupRegion = "ap-guangzhou"
33-
defaultMaxResults = 50
35+
defaultEventLimit = 20
36+
defaultMaxResults = defaultEventLimit
37+
defaultLookback = 24 * time.Hour
3438
maxResultsPerRequest = 50
3539
maxPages = 20
3640
)
3741

3842
// DumpEvents returns recent CloudAudit events. `args` is interpreted as an
39-
// optional `<startUnix>:<endUnix>` time window; pass "" to use the
40-
// CloudAudit default lookback (last 7 days).
43+
// optional `<startUnix>:<endUnix>` time window; pass "" or "all" to use an
44+
// explicit recent lookback window.
4145
func (d *Driver) DumpEvents(ctx context.Context, args string) ([]schema.Event, error) {
42-
startTime, endTime, err := parseTimeWindow(args)
46+
startTime, endTime, err := parseTimeWindow(args, d.now())
4347
if err != nil {
4448
return nil, err
4549
}
50+
accessKeyID := strings.TrimSpace(d.Credential.SecretID)
51+
if accessKeyID == "" {
52+
return nil, errors.New("tencent cloudaudit: empty secret id")
53+
}
4654
client := d.newClient()
4755
out := make([]schema.Event, 0)
4856
nextToken := ""
4957
for page := 0; page < maxPages; page++ {
50-
resp, err := client.LookUpEvents(ctx, defaultLookupRegion, startTime, endTime, defaultMaxResults, nextToken)
58+
resp, err := client.LookUpEvents(ctx, defaultLookupRegion, startTime, endTime, defaultMaxResults, nextToken, accessKeyID)
5159
if err != nil {
5260
return out, err
5361
}
5462
for _, ev := range resp.Response.Events {
63+
if len(out) >= defaultEventLimit {
64+
break
65+
}
5566
out = append(out, schema.Event{
56-
Id: derefString(ev.EventID),
57-
Name: derefString(ev.EventNameCn),
58-
Affected: derefString(ev.ResourceName),
59-
API: derefString(ev.EventName),
60-
Status: statusLabel(derefUint64(ev.Status)),
61-
SourceIp: derefString(ev.SourceIPAddress),
62-
AccessKey: derefString(ev.SecretID),
63-
Time: derefString(ev.EventTime),
67+
Id: derefString(ev.EventID),
68+
Name: derefString(ev.EventNameCn),
69+
Affected: derefString(ev.ResourceName),
70+
API: derefString(ev.EventName),
71+
Status: statusLabel(derefUint64(ev.Status)),
72+
SourceIp: derefString(ev.SourceIPAddress),
73+
// AccessKey: derefString(ev.SecretID),
74+
Time: formatEventTime(derefString(ev.EventTime)),
6475
})
6576
}
77+
if len(out) >= defaultEventLimit {
78+
break
79+
}
6680
if resp.Response.ListOver != nil && *resp.Response.ListOver {
6781
break
6882
}
69-
if resp.Response.NextToken == nil || *resp.Response.NextToken == "" {
83+
token := resp.Response.NextToken.String()
84+
if token == "" {
7085
break
7186
}
72-
nextToken = *resp.Response.NextToken
87+
nextToken = token
7388
}
7489
return out, nil
7590
}
@@ -82,10 +97,18 @@ func (d *Driver) HandleEvents(ctx context.Context, _ string) (schema.EventAction
8297
return schema.EventActionResult{}, errors.New("tencent cloudaudit: whitelist action is not supported (CloudAudit is read-only)")
8398
}
8499

85-
func parseTimeWindow(args string) (int64, int64, error) {
100+
func (d *Driver) now() time.Time {
101+
if d != nil && d.Clock != nil {
102+
return d.Clock().UTC()
103+
}
104+
return time.Now().UTC()
105+
}
106+
107+
func parseTimeWindow(args string, now time.Time) (int64, int64, error) {
86108
args = strings.TrimSpace(args)
87109
if args == "" || args == "all" {
88-
return 0, 0, nil
110+
end := now.UTC()
111+
return end.Add(-defaultLookback).Unix(), end.Unix(), nil
89112
}
90113
parts := strings.SplitN(args, ":", 2)
91114
if len(parts) != 2 {
@@ -124,6 +147,28 @@ func derefString(p *string) string {
124147
return *p
125148
}
126149

150+
func formatEventTime(value string) string {
151+
value = strings.TrimSpace(value)
152+
if value == "" {
153+
return ""
154+
}
155+
unix, err := strconv.ParseInt(value, 10, 64)
156+
if err != nil {
157+
return value
158+
}
159+
if unix <= 0 {
160+
return ""
161+
}
162+
switch {
163+
case unix > 999999999999999:
164+
return time.UnixMicro(unix).UTC().Format(time.RFC3339)
165+
case unix > 9999999999:
166+
return time.UnixMilli(unix).UTC().Format(time.RFC3339)
167+
default:
168+
return time.Unix(unix, 0).UTC().Format(time.RFC3339)
169+
}
170+
}
171+
127172
func derefUint64(p *uint64) uint64 {
128173
if p == nil {
129174
return 0

pkg/providers/tencent/cloudaudit/cloudaudit_test.go

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cloudaudit
22

33
import (
44
"context"
5+
"fmt"
6+
"io"
57
"net/http"
68
"net/http/httptest"
79
"strings"
@@ -14,10 +16,11 @@ import (
1416

1517
func newTestDriver(t *testing.T, baseURL string) *Driver {
1618
t.Helper()
17-
d := &Driver{Credential: auth.New("ak", "sk", "")}
19+
clock := func() time.Time { return time.Unix(1776458501, 0).UTC() }
20+
d := &Driver{Credential: auth.New("AKIDCURRENT", "sk", ""), Clock: clock}
1821
d.SetClientOptions(
1922
api.WithBaseURL(baseURL),
20-
api.WithClock(func() time.Time { return time.Unix(1776458501, 0).UTC() }),
23+
api.WithClock(clock),
2124
api.WithRetryPolicy(api.RetryPolicy{
2225
MaxAttempts: 1,
2326
Sleep: func(context.Context, time.Duration) error { return nil },
@@ -26,12 +29,24 @@ func newTestDriver(t *testing.T, baseURL string) *Driver {
2629
return d
2730
}
2831

29-
func TestDumpEventsMapsResponse(t *testing.T) {
32+
func TestDumpEventsPaginates(t *testing.T) {
33+
calls := 0
34+
var secondBody string
3035
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31-
if got := r.Header.Get("X-TC-Action"); got != "LookUpEvents" {
32-
t.Fatalf("unexpected action: %s", got)
36+
calls++
37+
switch calls {
38+
case 1:
39+
_, _ = w.Write([]byte(`{"Response":{"ListOver":false,"NextToken":2,"Events":[{"EventId":"e1","EventName":"CreateUser"}],"RequestId":"r1"}}`))
40+
case 2:
41+
body, err := io.ReadAll(r.Body)
42+
if err != nil {
43+
t.Fatalf("read body: %v", err)
44+
}
45+
secondBody = string(body)
46+
_, _ = w.Write([]byte(`{"Response":{"ListOver":true,"Events":[{"EventId":"e2","EventName":"DeleteUser"}],"RequestId":"r2"}}`))
47+
default:
48+
t.Fatalf("unexpected call: %d", calls)
3349
}
34-
_, _ = w.Write([]byte(`{"Response":{"ListOver":true,"Events":[{"EventId":"e1","EventName":"CreateUser","EventNameCn":"创建子用户","EventTime":"2026-04-22 09:10:11","EventRegion":"ap-guangzhou","Username":"alice","SourceIPAddress":"203.0.113.10","ResourceName":"ctk-demo-bot","Status":0,"SecretId":"AKID","ApiVersion":"2019-01-16"}],"RequestId":"r1"}}`))
3550
}))
3651
defer server.Close()
3752

@@ -40,33 +55,26 @@ func TestDumpEventsMapsResponse(t *testing.T) {
4055
if err != nil {
4156
t.Fatalf("DumpEvents: %v", err)
4257
}
43-
if len(got) != 1 {
44-
t.Fatalf("expected 1 event, got %d", len(got))
45-
}
46-
ev := got[0]
47-
if ev.Id != "e1" || ev.API != "CreateUser" || ev.Name != "创建子用户" {
48-
t.Errorf("unexpected event: %+v", ev)
49-
}
50-
if ev.SourceIp != "203.0.113.10" || ev.AccessKey != "AKID" {
51-
t.Errorf("unexpected event detail: %+v", ev)
58+
if len(got) != 2 {
59+
t.Fatalf("expected 2 events across pages, got %d", len(got))
5260
}
53-
if ev.Status != "成功" {
54-
t.Errorf("expected status '成功', got %q", ev.Status)
61+
if !strings.Contains(secondBody, `"NextToken":"2"`) {
62+
t.Fatalf("expected numeric response token to be sent as string, got %s", secondBody)
5563
}
5664
}
5765

58-
func TestDumpEventsPaginates(t *testing.T) {
59-
calls := 0
66+
func TestDumpEventsCapsDefaultOutputAndFormatsUnixTime(t *testing.T) {
6067
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61-
calls++
62-
switch calls {
63-
case 1:
64-
_, _ = w.Write([]byte(`{"Response":{"ListOver":false,"NextToken":"page-2","Events":[{"EventId":"e1","EventName":"CreateUser"}],"RequestId":"r1"}}`))
65-
case 2:
66-
_, _ = w.Write([]byte(`{"Response":{"ListOver":true,"Events":[{"EventId":"e2","EventName":"DeleteUser"}],"RequestId":"r2"}}`))
67-
default:
68-
t.Fatalf("unexpected call: %d", calls)
68+
var body strings.Builder
69+
body.WriteString(`{"Response":{"ListOver":true,"Events":[`)
70+
for i := 0; i < defaultEventLimit+5; i++ {
71+
if i > 0 {
72+
body.WriteByte(',')
73+
}
74+
_, _ = fmt.Fprintf(&body, `{"EventId":"e%d","EventName":"CreateUser","EventTime":"1778317184"}`, i)
6975
}
76+
body.WriteString(`],"RequestId":"r1"}}`)
77+
_, _ = w.Write([]byte(body.String()))
7078
}))
7179
defer server.Close()
7280

@@ -75,8 +83,15 @@ func TestDumpEventsPaginates(t *testing.T) {
7583
if err != nil {
7684
t.Fatalf("DumpEvents: %v", err)
7785
}
78-
if len(got) != 2 {
79-
t.Fatalf("expected 2 events across pages, got %d", len(got))
86+
if len(got) != defaultEventLimit {
87+
t.Fatalf("expected %d events, got %d", defaultEventLimit, len(got))
88+
}
89+
if got[defaultEventLimit-1].Id != "e19" {
90+
t.Fatalf("expected output to stop at e19, got %s", got[defaultEventLimit-1].Id)
91+
}
92+
wantTime := time.Unix(1778317184, 0).UTC().Format(time.RFC3339)
93+
if got[0].Time != wantTime {
94+
t.Fatalf("expected formatted event time %q, got %q", wantTime, got[0].Time)
8095
}
8196
}
8297

0 commit comments

Comments
 (0)