Skip to content

Commit 4873ccb

Browse files
JocLRojasmjabascal10osmontero
authored
Bugfix/10.6.2/update sophos integration guide (#1058)
* feat: add compliance reports * fix: update sophos guide * chore: update version and changelog * fix: update sophos guide * fix: update sophos guide * Enhance log alert message with key details. Include the specific blocklisted key in the alert message to provide clearer context and improve debugging efficiency. This update ensures more actionable and informative alerts. * Update how events are retrieved using the Sophos-Central API. --------- Co-authored-by: Manuel Abascal <[email protected]> Co-authored-by: Osmany Montero <[email protected]>
1 parent 71e530a commit 4873ccb

File tree

10 files changed

+1645
-54
lines changed

10 files changed

+1645
-54
lines changed

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
# UTMStack 10.6.1 Release Notes
2-
## Bug Fixes
3-
- Fixed ISM policy to ensure snapshots include only indices older than 24 hours.
1+
# UTMStack 10.6.2 Release Notes
2+
3+
## Features
4+
- Added additional compliance reports.
5+
- Updated the Sophos Central integration guide.

backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophos.java

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,32 +41,22 @@ public List<ModuleRequirement> checkRequirements(Long serverId) throws Exception
4141
public List<ModuleConfigurationKey> getConfigurationKeys(Long groupId) throws Exception {
4242
List<ModuleConfigurationKey> keys = new ArrayList<>();
4343

44-
// sophos_api_url
44+
// sophos_api_client_id
4545
keys.add(ModuleConfigurationKey.builder()
4646
.withGroupId(groupId)
47-
.withConfKey("sophos_api_url")
48-
.withConfName("API Url")
49-
.withConfDescription("Configure Sophos Central api url")
47+
.withConfKey("sophos_client_id")
48+
.withConfName("Client Id")
49+
.withConfDescription("Configure Sophos Central Client Id")
5050
.withConfDataType("text")
5151
.withConfRequired(true)
5252
.build());
5353

54-
// sophos_authorization
55-
keys.add(ModuleConfigurationKey.builder()
56-
.withGroupId(groupId)
57-
.withConfKey("sophos_authorization")
58-
.withConfName("Authorization")
59-
.withConfDescription("Configure Sophos Central Authorization")
60-
.withConfDataType("password")
61-
.withConfRequired(true)
62-
.build());
63-
64-
// sophos_x_api_key
54+
// sophos_x_client_secret
6555
keys.add(ModuleConfigurationKey.builder()
6656
.withGroupId(groupId)
6757
.withConfKey("sophos_x_api_key")
68-
.withConfName("X-API-KEY")
69-
.withConfDescription("Configure Sophos Central api key")
58+
.withConfName("Client Secret")
59+
.withConfDescription("Configure Sophos Central Client Secret")
7060
.withConfDataType("password")
7161
.withConfRequired(true)
7262
.build());

backend/src/main/resources/config/liquibase/changelog/20250227001_add_compliance_report.xml

Lines changed: 1460 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<databaseChangeLog
3+
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
6+
7+
<changeSet id="20250303001" author="Manuel">
8+
<sql>
9+
<![CDATA[
10+
DELETE FROM utm_module_group
11+
WHERE module_id = (SELECT id FROM utm_module WHERE module_name = 'SOPHOS');
12+
]]>
13+
</sql>
14+
</changeSet>
15+
16+
</databaseChangeLog>

backend/src/main/resources/config/liquibase/master.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373

7474
<include file="/config/liquibase/changelog/20250127001_add_compliance_data.xml" relativeToChangelogFile="false"/>
7575

76+
<include file="/config/liquibase/changelog/20250227001_add_compliance_report.xml" relativeToChangelogFile="false"/>
7677

78+
<include file="/config/liquibase/changelog/20250303001_udpate_sophos_guide.xml" relativeToChangelogFile="false"/>
7779

7880
</databaseChangeLog>

correlation/ti/ti.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func IsBlocklisted() {
9696

9797
if strings.Contains(log, key) {
9898
correlation.Alert(
99-
fmt.Sprintf("Maliciuos %s found in log", value),
99+
fmt.Sprintf("Malicious %s found in log: %s", value, key),
100100
"Low",
101101
"A blocklisted element has been identified in the logs. Further investigation is recommended.",
102102
"",

sophos/configuration/const.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import "github.com/utmstack/UTMStack/sophos/utils"
44

55
const (
66
CORRELATIONURL = "http://correlation:8080/v1/newlog"
7+
AUTHURL = "https://id.sophos.com/api/v2/oauth2/token"
8+
WHOAMIURL = "https://api.central.sophos.com/whoami/v1"
79
)
810

911
func GetInternalKey() string {

sophos/processor/processor.go

Lines changed: 146 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,181 @@ import (
44
"fmt"
55
"net/http"
66
"net/url"
7+
"time"
78

89
"github.com/threatwinds/logger"
10+
"github.com/utmstack/UTMStack/sophos/configuration"
911
"github.com/utmstack/UTMStack/sophos/utils"
1012
"github.com/utmstack/config-client-go/types"
1113
)
1214

1315
type SophosCentralProcessor struct {
14-
XApiKey string
15-
Authorization string
16-
ApiUrl string
16+
ClientID string
17+
ClientSecret string
18+
TenantID string
19+
DataRegion string
20+
AccessToken string
21+
ExpiresAt time.Time
1722
}
1823

19-
func GetSophosCentralProcessor(group types.ModuleGroup) SophosCentralProcessor {
24+
func getSophosCentralProcessor(group types.ModuleGroup) SophosCentralProcessor {
2025
sophosProcessor := SophosCentralProcessor{}
2126

2227
for _, cnf := range group.Configurations {
2328
switch cnf.ConfName {
24-
case "X-API-KEY":
25-
sophosProcessor.XApiKey = cnf.ConfValue
26-
case "Authorization":
27-
sophosProcessor.Authorization = cnf.ConfValue
28-
case "API Url":
29-
sophosProcessor.ApiUrl = cnf.ConfValue
29+
case "ClientID":
30+
sophosProcessor.ClientID = cnf.ConfValue
31+
case "ClientSecret":
32+
sophosProcessor.ClientSecret = cnf.ConfValue
3033
}
3134
}
3235
return sophosProcessor
3336
}
3437

35-
type EventAggregate struct {
36-
HasMore bool `json:"has_more"`
37-
Items []map[string]interface{} `json:"items"`
38-
NextCursor string `json:"next_cursor"`
39-
}
38+
func (p *SophosCentralProcessor) getAccessToken() (string, *logger.Error) {
39+
data := url.Values{}
40+
data.Set("grant_type", "client_credentials")
41+
data.Set("client_id", p.ClientID)
42+
data.Set("client_secret", p.ClientSecret)
43+
data.Set("scope", "token")
4044

41-
func (p *SophosCentralProcessor) GetLogs(group types.ModuleGroup, fromTime int) ([]TransformedLog, *logger.Error) {
42-
baseURL := p.ApiUrl + "/siem/v1/events"
45+
headers := map[string]string{
46+
"Content-Type": "application/x-www-form-urlencoded",
47+
}
4348

44-
u, parseerr := url.Parse(baseURL)
45-
if parseerr != nil {
46-
return nil, utils.Logger.ErrorF("error parsing URL params: %v", parseerr)
49+
response, _, err := utils.DoReq[map[string]any](configuration.AUTHURL, []byte(data.Encode()), http.MethodPost, headers)
50+
if err != nil {
51+
return "", utils.Logger.ErrorF("error making auth request: %v", err)
4752
}
4853

49-
params := url.Values{}
50-
params.Add("limit", "1000")
51-
params.Add("from_date", fmt.Sprintf("%d", fromTime))
54+
accessToken, ok := response["access_token"].(string)
55+
if !ok || accessToken == "" {
56+
return "", utils.Logger.ErrorF("access_token not found in response")
57+
}
5258

53-
u.RawQuery = params.Encode()
59+
expiresIn, ok := response["expires_in"].(float64)
60+
if !ok {
61+
return "", utils.Logger.ErrorF("expires_in not found in response")
62+
}
63+
64+
p.AccessToken = accessToken
65+
p.ExpiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second)
66+
67+
return accessToken, nil
68+
}
69+
70+
type WhoamiResponse struct {
71+
ID string `json:"id"`
72+
ApiHosts ApiHosts `json:"apiHosts"`
73+
}
74+
type ApiHosts struct {
75+
Global string `json:"global"`
76+
DataRegion string `json:"dataRegion"`
77+
}
5478

79+
func (p *SophosCentralProcessor) getTenantInfo(accessToken string) *logger.Error {
5580
headers := map[string]string{
5681
"accept": "application/json",
57-
"Authorization": p.Authorization,
58-
"x-api-key": p.XApiKey,
82+
"Authorization": "Bearer " + accessToken,
5983
}
6084

61-
response, _, err := utils.DoReq[EventAggregate](u.String(), nil, http.MethodGet, headers)
85+
response, _, err := utils.DoReq[WhoamiResponse](configuration.WHOAMIURL, nil, http.MethodGet, headers)
6286
if err != nil {
63-
return nil, err
87+
return utils.Logger.ErrorF("error making whoami request: %v", err)
88+
}
89+
90+
if response.ID == "" {
91+
return utils.Logger.ErrorF("tenant ID not found in whoami response")
92+
}
93+
p.TenantID = response.ID
94+
95+
if response.ApiHosts.DataRegion == "" {
96+
return utils.Logger.ErrorF("dataRegion not found in whoami response")
97+
}
98+
p.DataRegion = response.ApiHosts.DataRegion
99+
100+
return nil
101+
}
102+
103+
func (p *SophosCentralProcessor) getValidAccessToken() (string, *logger.Error) {
104+
if p.AccessToken != "" && time.Now().Before(p.ExpiresAt) {
105+
return p.AccessToken, nil
64106
}
107+
return p.getAccessToken()
108+
}
65109

66-
logs := ETLProcess(response, group)
67-
return logs, nil
110+
type EventAggregate struct {
111+
Pages Pages `json:"pages"`
112+
Items []map[string]any `json:"items"`
113+
}
114+
115+
type Pages struct {
116+
FromKey string `json:"fromKey"`
117+
NextKey string `json:"nextKey"`
118+
Size int64 `json:"size"`
119+
MaxSize int64 `json:"maxSize"`
120+
}
121+
122+
func (p *SophosCentralProcessor) getLogs(fromTime int, nextKey string, group types.ModuleGroup) ([]TransformedLog, string, *logger.Error) {
123+
accessToken, err := p.getValidAccessToken()
124+
if err != nil {
125+
return nil, "", utils.Logger.ErrorF("error getting access token: %v", err)
126+
}
127+
128+
if p.TenantID == "" || p.DataRegion == "" {
129+
if err := p.getTenantInfo(accessToken); err != nil {
130+
return nil, "", utils.Logger.ErrorF("error getting tenant information: %v", err)
131+
}
132+
}
133+
134+
var aggregatedEvents EventAggregate
135+
aggregatedEvents.Items = make([]map[string]any, 0)
136+
currentNextKey := nextKey
137+
138+
for {
139+
u, err := p.buildURL(fromTime, currentNextKey)
140+
if err != nil {
141+
return nil, "", utils.Logger.ErrorF("error building URL: %v", err)
142+
}
143+
144+
headers := map[string]string{
145+
"Content-Type": "application/json",
146+
"Authorization": "Bearer " + accessToken,
147+
"X-Tenant-ID": p.TenantID,
148+
}
149+
150+
response, _, err := utils.DoReq[EventAggregate](u.String(), nil, http.MethodGet, headers)
151+
if err != nil {
152+
return nil, "", err
153+
}
154+
155+
aggregatedEvents.Items = append(aggregatedEvents.Items, response.Items...)
156+
157+
if response.Pages.NextKey == "" {
158+
break
159+
}
160+
currentNextKey = response.Pages.NextKey
161+
}
162+
163+
transformedLogs := ETLProcess(aggregatedEvents, group)
164+
165+
return transformedLogs, currentNextKey, nil
166+
}
167+
168+
func (p *SophosCentralProcessor) buildURL(fromTime int, nextKey string) (*url.URL, *logger.Error) {
169+
baseURL := p.DataRegion + "/siem/v1/events"
170+
u, parseErr := url.Parse(baseURL)
171+
if parseErr != nil {
172+
return nil, utils.Logger.ErrorF("error parsing url: %v", parseErr)
173+
}
174+
175+
params := url.Values{}
176+
if nextKey != "" {
177+
params.Set("pageFromKey", nextKey)
178+
} else {
179+
params.Set("from_date", fmt.Sprintf("%d", fromTime))
180+
}
181+
182+
u.RawQuery = params.Encode()
183+
return u, nil
68184
}

sophos/processor/pull.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
const delayCheck = 300
1212

1313
var timeGroups = make(map[int]int)
14+
var nextKeys = make(map[int]string)
1415

1516
func PullLogs(group types.ModuleGroup) *logger.Error {
1617
utils.Logger.Info("starting log sync for : %s", group.GroupName)
@@ -26,13 +27,15 @@ func PullLogs(group types.ModuleGroup) *logger.Error {
2627
timeGroups[group.ModuleID] = epoch + 1
2728
}()
2829

29-
agent := GetSophosCentralProcessor(group)
30+
agent := getSophosCentralProcessor(group)
3031

31-
logs, err := agent.GetLogs(group, timeGroups[group.ModuleID])
32+
logs, newNextKey, err := agent.getLogs(timeGroups[group.ModuleID], nextKeys[group.ModuleID], group)
3233
if err != nil {
3334
return err
3435
}
3536

37+
nextKeys[group.ModuleID] = newNextKey
38+
3639
err = SendToCorrelation(logs)
3740
if err != nil {
3841
return err

version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version: 10.6.1
1+
version: 10.6.2

0 commit comments

Comments
 (0)