sophos-central-mcp-server 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ export interface SophosConfig {
12
12
  export declare function loadConfig(): SophosConfig;
13
13
  export declare const SOPHOS_AUTH_URL = "https://id.sophos.com/api/v2/oauth2/token";
14
14
  export declare const SOPHOS_GLOBAL_API = "https://api.central.sophos.com";
15
- export declare const CHARACTER_LIMIT = 25000;
15
+ export declare const CHARACTER_LIMIT: number;
16
16
  export declare const DEFAULT_PAGE_SIZE = 50;
17
17
  export declare const MAX_PAGE_SIZE = 100;
18
18
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B;AAED,wBAAgB,UAAU,IAAI,YAAY,CAwBzC;AAGD,eAAO,MAAM,eAAe,8CAA8C,CAAC;AAC3E,eAAO,MAAM,iBAAiB,mCAAmC,CAAC;AAGlE,eAAO,MAAM,eAAe,QAAQ,CAAC;AACrC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,aAAa,MAAM,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B;AAED,wBAAgB,UAAU,IAAI,YAAY,CAwBzC;AAGD,eAAO,MAAM,eAAe,8CAA8C,CAAC;AAC3E,eAAO,MAAM,iBAAiB,mCAAmC,CAAC;AAGlE,eAAO,MAAM,eAAe,QAG3B,CAAC;AACF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,aAAa,MAAM,CAAC"}
@@ -23,8 +23,8 @@ export function loadConfig() {
23
23
  // Sophos Central global API endpoints
24
24
  export const SOPHOS_AUTH_URL = "https://id.sophos.com/api/v2/oauth2/token";
25
25
  export const SOPHOS_GLOBAL_API = "https://api.central.sophos.com";
26
- // Response size limits
27
- export const CHARACTER_LIMIT = 25000;
26
+ // Response size limits (CHARACTER_LIMIT configurable via env var)
27
+ export const CHARACTER_LIMIT = Math.max(10000, parseInt(process.env.CHARACTER_LIMIT || "50000", 10) || 50000);
28
28
  export const DEFAULT_PAGE_SIZE = 50;
29
29
  export const MAX_PAGE_SIZE = 100;
30
30
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,MAAM,UAAU,UAAU;IACxB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC9C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAEtD,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAY,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,gGAAgG,CACjG,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,KAAK,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CACb,uBAAuB,OAAO,CAAC,GAAG,CAAC,IAAI,4CAA4C,CACpF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ;QACR,YAAY;QACZ,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,SAAS;QACnD,IAAI,EAAE,OAAO;QACb,SAAS,EAAG,OAAO,CAAC,GAAG,CAAC,SAA8B,IAAI,MAAM;KACjE,CAAC;AACJ,CAAC;AAED,sCAAsC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,2CAA2C,CAAC;AAC3E,MAAM,CAAC,MAAM,iBAAiB,GAAG,gCAAgC,CAAC;AAElE,uBAAuB;AACvB,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AACrC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,MAAM,UAAU,UAAU;IACxB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC9C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAEtD,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAY,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,gGAAgG,CACjG,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,KAAK,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CACb,uBAAuB,OAAO,CAAC,GAAG,CAAC,IAAI,4CAA4C,CACpF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ;QACR,YAAY;QACZ,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,SAAS;QACnD,IAAI,EAAE,OAAO;QACb,SAAS,EAAG,OAAO,CAAC,GAAG,CAAC,SAA8B,IAAI,MAAM;KACjE,CAAC;AACJ,CAAC;AAED,sCAAsC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,2CAA2C,CAAC;AAC3E,MAAM,CAAC,MAAM,iBAAiB,GAAG,gCAAgC,CAAC;AAElE,kEAAkE;AAClE,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CACrC,KAAK,EACL,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,EAAE,EAAE,CAAC,IAAI,KAAK,CAC9D,CAAC;AACF,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC"}
@@ -5,7 +5,7 @@ sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
5
5
  from fpdf import FPDF
6
6
  from datetime import datetime
7
7
 
8
- # ── Sophos brand colours ──
8
+ # -- Sophos brand colours --
9
9
  SOPHOS_BLUE = (0, 53, 128)
10
10
  SOPHOS_DARK = (34, 34, 34)
11
11
  SOPHOS_RED = (200, 30, 30)
@@ -14,192 +14,277 @@ SOPHOS_GREEN = (40, 140, 60)
14
14
  SOPHOS_GREY = (100, 100, 100)
15
15
  WHITE = (255, 255, 255)
16
16
  LIGHT_BG = (245, 247, 250)
17
- ROW_ALT = (235, 240, 248)
17
+ ROW_ALT = (232, 238, 248)
18
+
19
+ PAGE_W = 190 # usable width (A4 210mm - 10mm margins each side)
20
+
18
21
 
19
22
  class PDF(FPDF):
23
+ """Custom PDF with Sophos-branded header/footer."""
24
+
25
+ _skip_header = False # flag for cover page
26
+
20
27
  def header(self):
21
- self.set_font("Helvetica", "B", 9)
28
+ if self._skip_header:
29
+ return
30
+ self.set_font("Helvetica", "B", 8)
22
31
  self.set_text_color(*SOPHOS_GREY)
23
- self.cell(0, 6, "Rodin Business Solutions | Sophos Central Estate Health Report | March 2026", align="L")
24
- self.ln(4)
32
+ self.cell(0, 5, "Rodin Business Solutions | Sophos Central Estate Health Report | March 2026", align="L")
33
+ self.ln(3)
25
34
  self.set_draw_color(*SOPHOS_BLUE)
26
- self.set_line_width(0.6)
35
+ self.set_line_width(0.5)
27
36
  self.line(10, self.get_y(), 200, self.get_y())
28
- self.ln(6)
37
+ self.ln(5)
29
38
 
30
39
  def footer(self):
31
40
  self.set_y(-15)
32
41
  self.set_font("Helvetica", "", 8)
33
42
  self.set_text_color(*SOPHOS_GREY)
34
- self.cell(0, 10, f"Page {self.page_no()}/{{nb}} | Generated {datetime.now().strftime('%d/%m/%Y %H:%M')}", align="C")
43
+ self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C")
44
+
45
+ # -- Helpers ----------------------------------------------------------
35
46
 
36
47
  def section_title(self, title, colour=SOPHOS_BLUE):
37
48
  self.set_font("Helvetica", "B", 14)
38
49
  self.set_text_color(*colour)
39
50
  self.cell(0, 10, title)
40
- self.ln(8)
51
+ self.ln(9)
41
52
  self.set_text_color(*SOPHOS_DARK)
42
53
 
43
54
  def sub_title(self, title, colour=SOPHOS_DARK):
44
55
  self.set_font("Helvetica", "B", 11)
45
56
  self.set_text_color(*colour)
46
57
  self.cell(0, 8, title)
47
- self.ln(6)
58
+ self.ln(7)
48
59
  self.set_text_color(*SOPHOS_DARK)
49
60
 
50
- def body_text(self, text):
51
- self.set_font("Helvetica", "", 10)
61
+ def body_text(self, text, size=10):
62
+ self.set_font("Helvetica", "", size)
52
63
  self.set_text_color(*SOPHOS_DARK)
53
64
  self.multi_cell(0, 5.5, text)
54
65
  self.ln(2)
55
66
 
56
67
  def table_header(self, cols):
57
- self.set_font("Helvetica", "B", 9)
68
+ """Draw a header row. cols = [(label, width), ...]"""
69
+ self.set_font("Helvetica", "B", 8)
58
70
  self.set_fill_color(*SOPHOS_BLUE)
59
71
  self.set_text_color(*WHITE)
60
72
  for label, w in cols:
61
- self.cell(w, 7, label, border=0, fill=True, align="C")
73
+ self.cell(w, 7, f" {label}", border=0, fill=True, align="L")
62
74
  self.ln()
63
75
  self.set_text_color(*SOPHOS_DARK)
64
76
 
65
- def table_row(self, cols, row_idx=0):
66
- self.set_font("Helvetica", "", 9)
77
+ def _row_height(self, cols, font_size=8):
78
+ """Calculate the row height needed for the tallest multi-cell."""
79
+ self.set_font("Helvetica", "", font_size)
80
+ max_lines = 1
81
+ for val, w, _align in cols:
82
+ text = str(val)
83
+ # Estimate number of lines: char width ~2.1mm at size 8
84
+ char_w = font_size * 0.23
85
+ chars_per_line = max(int(w / char_w), 1)
86
+ lines = 1
87
+ for paragraph in text.split("\n"):
88
+ lines += max(0, (len(paragraph) - 1) // chars_per_line)
89
+ if len(paragraph) > 0:
90
+ pass
91
+ lines += 0 # already counted
92
+ # Simpler: use fpdf's internal string width
93
+ str_w = self.get_string_width(text)
94
+ est_lines = max(1, int(str_w / (w - 2)) + 1)
95
+ max_lines = max(max_lines, est_lines)
96
+ return max(6.5, max_lines * 4.2)
97
+
98
+ def table_row(self, cols, row_idx=0, font_size=8):
99
+ """Draw a data row with alternating colours. cols = [(val, width, align), ...]"""
100
+ self.set_font("Helvetica", "", font_size)
67
101
  if row_idx % 2 == 1:
68
102
  self.set_fill_color(*ROW_ALT)
69
- fill = True
70
103
  else:
71
104
  self.set_fill_color(*WHITE)
72
- fill = True
105
+
106
+ # Check if we need a page break (leave room for row + footer)
107
+ row_h = 6.5
108
+ if self.get_y() + row_h > self.h - self.b_margin - 5:
109
+ self.add_page()
110
+
73
111
  for val, w, align in cols:
74
- self.cell(w, 6.5, str(val), border=0, fill=fill, align=align)
112
+ self.cell(w, row_h, f" {str(val)}", border=0, fill=True, align=align)
75
113
  self.ln()
76
114
 
115
+ def table_row_multi(self, cols, row_idx=0, row_h=6.5, font_size=8):
116
+ """Draw a row that may wrap text in the last column."""
117
+ self.set_font("Helvetica", "", font_size)
118
+ if row_idx % 2 == 1:
119
+ self.set_fill_color(*ROW_ALT)
120
+ else:
121
+ self.set_fill_color(*WHITE)
122
+
123
+ if self.get_y() + row_h > self.h - self.b_margin - 5:
124
+ self.add_page()
125
+
126
+ x_start = self.get_x()
127
+ y_start = self.get_y()
128
+
129
+ # Draw fixed cells
130
+ for val, w, align in cols[:-1]:
131
+ self.cell(w, row_h, f" {str(val)}", border=0, fill=True, align=align)
132
+
133
+ # Draw last cell as multi_cell for wrapping
134
+ last_val, last_w, last_align = cols[-1]
135
+ x_last = self.get_x()
136
+ self.multi_cell(last_w, row_h / max(1, len(str(last_val)) // 40 + 1),
137
+ f" {str(last_val)}", border=0, fill=True, align=last_align)
77
138
 
139
+ y_end = self.get_y()
140
+ actual_h = y_end - y_start
141
+ if actual_h < row_h:
142
+ self.set_y(y_start + row_h)
143
+
144
+ def check_space(self, needed_mm=30):
145
+ """Add a page if less than needed_mm space remains."""
146
+ if self.get_y() + needed_mm > self.h - self.b_margin:
147
+ self.add_page()
148
+
149
+
150
+ # ===========================================================================
151
+ # BUILD THE PDF
152
+ # ===========================================================================
78
153
  pdf = PDF(orientation="P", unit="mm", format="A4")
79
154
  pdf.alias_nb_pages()
80
155
  pdf.set_auto_page_break(auto=True, margin=20)
81
156
 
82
- # ═══════════════════════════════════════════════════════════════════════════════
83
- # COVER / EXECUTIVE SUMMARY
84
- # ═══════════════════════════════════════════════════════════════════════════════
157
+ # ===== COVER PAGE ==========================================================
158
+ pdf._skip_header = True
85
159
  pdf.add_page()
86
- pdf.ln(15)
87
- pdf.set_font("Helvetica", "B", 28)
160
+ pdf.ln(30)
161
+
162
+ # Title block
163
+ pdf.set_font("Helvetica", "B", 30)
88
164
  pdf.set_text_color(*SOPHOS_BLUE)
89
165
  pdf.cell(0, 14, "Sophos Central", align="C")
90
- pdf.ln(14)
91
- pdf.set_font("Helvetica", "B", 22)
92
- pdf.cell(0, 12, "Estate Health & Opportunity Report", align="C")
93
166
  pdf.ln(16)
94
- pdf.set_font("Helvetica", "", 13)
95
- pdf.set_text_color(*SOPHOS_GREY)
96
- pdf.cell(0, 8, "Rodin Business Solutions | March 2026", align="C")
97
- pdf.ln(8)
98
- pdf.cell(0, 8, "Prepared by Sophos Central MCP Automation", align="C")
99
- pdf.ln(20)
167
+ pdf.set_font("Helvetica", "B", 20)
168
+ pdf.cell(0, 12, "Estate Health & Opportunity Report", align="C")
169
+ pdf.ln(18)
100
170
 
101
- # Summary boxes
171
+ # Decorative line
102
172
  pdf.set_draw_color(*SOPHOS_BLUE)
103
- pdf.set_line_width(0.4)
173
+ pdf.set_line_width(1.0)
174
+ pdf.line(60, pdf.get_y(), 150, pdf.get_y())
175
+ pdf.ln(12)
176
+
177
+ pdf.set_font("Helvetica", "", 14)
178
+ pdf.set_text_color(*SOPHOS_GREY)
179
+ pdf.cell(0, 8, "Rodin Business Solutions | March 2026", align="C")
180
+ pdf.ln(30)
104
181
 
182
+ # Summary metric boxes
105
183
  metrics = [
106
184
  ("153", "Total Tenants"),
107
185
  ("115", "Healthy (75%)"),
108
186
  ("34", "With Gaps (22%)"),
109
- ("$38,923", "Monthly AUD"),
187
+ ("$38,923/mo", "Monthly AUD"),
110
188
  ]
111
189
  box_w = 42
112
- start_x = 12
190
+ gap = 4
191
+ total_w = len(metrics) * box_w + (len(metrics) - 1) * gap
192
+ start_x = (210 - total_w) / 2
193
+ y_boxes = pdf.get_y()
194
+
113
195
  for i, (val, label) in enumerate(metrics):
114
- x = start_x + i * (box_w + 4)
115
- pdf.set_xy(x, pdf.get_y())
196
+ x = start_x + i * (box_w + gap)
116
197
  pdf.set_fill_color(*LIGHT_BG)
117
- pdf.rect(x, pdf.get_y(), box_w, 24, style="FD")
118
- pdf.set_xy(x, pdf.get_y() + 3)
119
- pdf.set_font("Helvetica", "B", 18)
198
+ pdf.set_draw_color(*SOPHOS_BLUE)
199
+ pdf.set_line_width(0.4)
200
+ pdf.rect(x, y_boxes, box_w, 26, style="FD")
201
+ pdf.set_xy(x, y_boxes + 4)
202
+ pdf.set_font("Helvetica", "B", 20)
120
203
  pdf.set_text_color(*SOPHOS_BLUE)
121
204
  pdf.cell(box_w, 10, val, align="C")
122
- pdf.set_xy(x, pdf.get_y() + 10)
205
+ pdf.set_xy(x, y_boxes + 15)
123
206
  pdf.set_font("Helvetica", "", 9)
124
207
  pdf.set_text_color(*SOPHOS_GREY)
125
208
  pdf.cell(box_w, 6, label, align="C")
126
209
 
127
- pdf.ln(32)
210
+ pdf.set_y(y_boxes + 36)
128
211
 
212
+ # Executive summary
129
213
  pdf.set_text_color(*SOPHOS_DARK)
130
214
  pdf.section_title("Executive Summary")
131
215
  pdf.body_text(
132
216
  "This report provides a comprehensive health assessment of the Rodin Business Solutions "
133
- "managed Sophos Central estate for March 2026. It identifies security configuration gaps "
134
- "requiring remediation and highlights commercial opportunities where customers could benefit "
135
- "from additional Sophos products."
217
+ "managed Sophos Central estate for March 2026. Across 153 managed tenants, 75% are fully "
218
+ "healthy with no configuration gaps detected. 22% have identifiable security or licensing "
219
+ "gaps that require remediation. The remaining tenants represent commercial upsell opportunities."
136
220
  )
137
221
  pdf.body_text(
138
222
  "Key findings:\n"
139
- "- 4 tenants have tamper protection completely disabled (0%) - critical risk\n"
140
- "- 1 tenant has server protection at 0% despite having enrolled servers\n"
141
- "- 18 tenants have partial tamper protection gaps across computers or servers\n"
142
- "- 14 tenants have no server protection enrolled (upsell opportunity)\n"
143
- "- Multiple customers lack MDR or Sophos Firewall coverage (detailed in Section 4)"
223
+ " - 4 tenants have tamper protection completely disabled (0%) representing critical risk\n"
224
+ " - 1 tenant has server protection at 0% despite having enrolled servers\n"
225
+ " - 6 tenants have high-priority partial tamper protection gaps on servers\n"
226
+ " - 10 tenants have moderate gaps (92-99% TP) likely from stale devices\n"
227
+ " - 14 tenants have no server protection enrolled\n"
228
+ " - Multiple customers lack MDR or Sophos Firewall coverage (detailed in sections 5-7)"
144
229
  )
145
230
 
146
- # ═══════════════════════════════════════════════════════════════════════════════
147
- # SECTION 2: CRITICAL HEALTH GAPS
148
- # ═══════════════════════════════════════════════════════════════════════════════
231
+ pdf._skip_header = False
232
+
233
+ # ===== SECTION 2: CRITICAL HEALTH GAPS ====================================
149
234
  pdf.add_page()
150
235
  pdf.section_title("1. Critical Health Gaps", SOPHOS_RED)
151
236
  pdf.body_text(
152
237
  "The following tenants have tamper protection at 0% or server protection failures. "
153
- "These represent the highest security risk and should be remediated immediately. "
154
- "Without tamper protection, a threat actor who gains access can disable Sophos endpoint "
155
- "protection entirely."
238
+ "These represent the highest security risk and must be remediated immediately. "
239
+ "Without tamper protection, a threat actor who gains endpoint access can disable "
240
+ "Sophos protection entirely."
156
241
  )
157
242
 
158
243
  critical = [
159
- ("Marco Polo Aged Care Services", "TP 0% computers AND servers", "Re-enable global TP immediately - aged care is high compliance risk"),
160
- ("Unibuild", "TP 0% computers", "Re-enable global TP; check if intentionally disabled during deployment"),
244
+ ("Marco Polo Aged Care Services", "TP 0% computers AND servers", "Re-enable global TP immediately"),
245
+ ("Unibuild", "TP 0% computers", "Re-enable global TP"),
161
246
  ("MasterCraft Windows and Doors", "TP 0% computers", "Re-enable global TP"),
162
- ("Martin Morris & Jones Sydney", "TP 0% computers", "Re-enable global TP; may be a stale/test tenant"),
163
- ("WIN Sports & Entertainment Centres", "Server protection 0% (2 servers)", "Investigate why 2 enrolled servers show 0% protection"),
247
+ ("Martin Morris & Jones Sydney", "TP 0% computers", "Re-enable global TP"),
248
+ ("WIN Sports & Entertainment Centres", "Server protection 0% (2 servers)", "Investigate enrolled servers"),
164
249
  ]
165
250
 
166
- cols = [("Customer", 55), ("Issue", 55), ("Recommended Action", 80)]
167
- pdf.table_header(cols)
251
+ cols_def = [("Customer", 58), ("Issue", 58), ("Recommended Action", 74)]
252
+ pdf.table_header(cols_def)
168
253
  for i, (cust, issue, action) in enumerate(critical):
169
- pdf.table_row([(cust, 55, "L"), (issue, 55, "L"), (action, 80, "L")], i)
254
+ pdf.table_row([(cust, 58, "L"), (issue, 58, "L"), (action, 74, "L")], i)
170
255
 
171
- pdf.ln(6)
256
+ pdf.ln(8)
172
257
 
173
- # ═══════════════════════════════════════════════════════════════════════════════
174
- # SECTION 3: HIGH & MODERATE GAPS
175
- # ═══════════════════════════════════════════════════════════════════════════════
258
+ # ===== SECTION 3: HIGH PRIORITY GAPS ======================================
176
259
  pdf.section_title("2. High Priority Gaps", SOPHOS_AMBER)
177
260
  pdf.body_text(
178
261
  "These tenants have tamper protection below 90% on servers or significant partial gaps. "
179
- "In most cases this indicates 1-2 endpoints with TP disabled that need attention."
262
+ "In most cases, 1-2 specific endpoints or servers need TP re-enabled."
180
263
  )
181
264
 
182
265
  high = [
183
- ("Durham Green", "TP 59% computers", "Multiple endpoints with TP off - audit and re-enable"),
184
- ("Southern Truck Centre", "TP 99% PC, 66% servers", "1 of 3 servers has TP disabled"),
266
+ ("Durham Green", "TP 59% computers", "Audit and re-enable on multiple endpoints"),
267
+ ("Southern Truck Centre", "TP 99% PC, 66% servers", "1 of 3 servers TP disabled"),
185
268
  ("Gunderson Briggs", "TP 98% PC, 88% servers", "1 server + 1 computer TP disabled"),
186
- ("IOH Injury & Occupational Health", "TP 99% PC, 88% servers", "1 server with TP disabled"),
187
- ("Project Portfolio Management", "TP 80% servers", "1 server with TP disabled"),
188
- ("Sydney Automotive Paints & Equipment", "TP 85% servers", "1 server with TP disabled"),
269
+ ("IOH Injury & Occupational Health", "TP 99% PC, 88% servers", "1 server TP disabled"),
270
+ ("Project Portfolio Management", "TP 80% servers", "1 server TP disabled"),
271
+ ("Sydney Automotive Paints & Equipment", "TP 85% servers", "1 server TP disabled"),
189
272
  ]
190
273
 
191
- cols = [("Customer", 60), ("Issue", 60), ("Action", 70)]
192
- pdf.table_header(cols)
274
+ cols_def = [("Customer", 62), ("Issue", 60), ("Action", 68)]
275
+ pdf.table_header(cols_def)
193
276
  for i, (cust, issue, action) in enumerate(high):
194
- pdf.table_row([(cust, 60, "L"), (issue, 60, "L"), (action, 70, "L")], i)
277
+ pdf.table_row([(cust, 62, "L"), (issue, 60, "L"), (action, 68, "L")], i)
195
278
 
196
- pdf.ln(6)
279
+ pdf.ln(8)
197
280
 
198
- pdf.section_title("3. Moderate Gaps - Cleanup Required")
281
+ # ===== SECTION 4: MODERATE GAPS ===========================================
282
+ pdf.check_space(60)
283
+ pdf.section_title("3. Moderate Gaps (Cleanup Required)")
199
284
  pdf.body_text(
200
285
  "These tenants have tamper protection between 92-99%. Typically 1-2 stale or "
201
- "decommissioned endpoints. Recommend checking if devices are still active and either "
202
- "re-enabling TP or removing the stale device."
286
+ "decommissioned endpoints. Check if devices are still active; re-enable TP or "
287
+ "remove stale registrations."
203
288
  )
204
289
 
205
290
  moderate = [
@@ -215,152 +300,200 @@ moderate = [
215
300
  ("Cram Foundation", "99%", "1 endpoint"),
216
301
  ]
217
302
 
218
- cols = [("Customer", 70), ("TP Score", 30), ("Likely Scope", 70)]
219
- pdf.table_header(cols)
303
+ cols_def = [("Customer", 80), ("TP Score", 40), ("Likely Scope", 70)]
304
+ pdf.table_header(cols_def)
220
305
  for i, (cust, score, scope) in enumerate(moderate):
221
- pdf.table_row([(cust, 70, "L"), (score, 30, "C"), (scope, 70, "L")], i)
306
+ pdf.table_row([(cust, 80, "L"), (score, 40, "C"), (scope, 70, "L")], i)
222
307
 
223
- # ═══════════════════════════════════════════════════════════════════════════════
224
- # SECTION 4: UPSELL - NO MDR
225
- # ═══════════════════════════════════════════════════════════════════════════════
308
+ # ===== SECTION 5: UPSELL - NO MDR =========================================
226
309
  pdf.add_page()
227
- pdf.section_title("4. Upsell Opportunities")
228
-
229
- pdf.sub_title("4a. Customers Without MDR", SOPHOS_BLUE)
310
+ pdf.section_title("4. Upsell: Customers Without MDR")
230
311
  pdf.body_text(
231
- "The following customers are on Sophos Endpoint only (no MDR Complete or Essentials). "
232
- "These represent the highest-value upsell opportunity - MDR adds 24/7 threat detection "
233
- "and response with the Sophos MDR team. Sorted by user count to prioritise larger estates."
312
+ "The following customers are on Sophos Endpoint only with no MDR coverage. "
313
+ "MDR adds 24/7 threat detection and response by the Sophos MDR team. "
314
+ "Sorted by monthly spend to prioritise the largest existing relationships."
234
315
  )
235
316
 
236
317
  no_mdr = [
237
- ("Southern Truck Centre Wollongong", 41, 2, "$446.91", "Endpoint + XG 210 only. 43 protected assets with no MDR coverage."),
238
- ("Kintyre Country Living", 3, 0, "$415.77", "XG 310 firewall + 3 users. Low user count but paying for premium firewall."),
239
- ("Custom Built Frames & Trusses", 3, 1, "$33.57", "Endpoint only + XGS 107. Small but could bundle MDR with existing firewall."),
240
- ("Gunderson Briggs", 23, 2, "$136.11", "Endpoint only + XGS 136. 25 assets - strong MDR candidate."),
241
- ("The Illawarra ITEC Ltd", 3, 1, "$14.21", "Endpoint only. Small tenant."),
242
- ("Tulich Project Management", 0, 2, "$13.24", "Servers only, no MDR. Should have at minimum MDR Essentials on servers."),
318
+ ("Southern Truck Centre Wollongong", 41, 2, "$446.91", "Endpoint + XG 210", "Strong MDR candidate (43 assets)"),
319
+ ("Kintyre Country Living", 3, 0, "$415.77", "XG 310 + Endpoint", "Low user count but premium FW"),
320
+ ("REI Process", 0, 0, "$392.30", "XGS 2100 + SF Virtual", "Device-only - check if endpoints needed"),
321
+ ("University of Wollongong SmartSpace", 0, 0, "$357.72", "XGS 3300", "Single large firewall"),
322
+ ("Autopact", 0, 0, "$306.32", "XGS 126 + SF Virtual", "Device-only estate"),
323
+ ("IOH Injury & Occupational Health", 0, 0, "$285.38", "Multiple firewalls", "7 devices no MDR"),
324
+ ("Community Gateway Limited", 0, 0, "$181.28", "XGS 126 + XGS 2100", "Firewall-only"),
325
+ ("Martin Morris & Jones Wollongong", 0, 0, "$159.72", "XGS 2100 + XGS 107", "Firewall-only"),
326
+ ("Gunderson Briggs", 23, 2, "$136.11", "Endpoint + XGS 136", "25 assets - strong candidate"),
327
+ ("Custom Built Frames & Trusses", 3, 1, "$33.57", "Endpoint + XGS 107", "Small but bundleable"),
328
+ ("The Illawarra ITEC Ltd", 3, 1, "$14.21", "Endpoint only", "Small tenant"),
329
+ ("Tulich Project Management", 0, 2, "$13.24", "Server Endpoint only", "Needs MDR Essentials minimum"),
243
330
  ]
244
331
 
245
- cols = [("Customer", 52), ("Users", 14), ("Servers", 14), ("Current $", 22), ("Notes", 88)]
246
- pdf.table_header(cols)
247
- for i, (cust, users, servers, rev, notes) in enumerate(no_mdr):
248
- pdf.table_row([(cust, 52, "L"), (str(users), 14, "C"), (str(servers), 14, "C"), (rev, 22, "R"), (notes, 88, "L")], i)
332
+ cols_def = [("Customer", 52), ("Users", 12), ("Svrs", 12), ("Monthly $", 20), ("Current Products", 42), ("Opportunity", 52)]
333
+ pdf.table_header(cols_def)
334
+ for i, (cust, users, servers, rev, products, opp) in enumerate(no_mdr):
335
+ pdf.table_row([
336
+ (cust, 52, "L"), (str(users), 12, "C"), (str(servers), 12, "C"),
337
+ (rev, 20, "R"), (products, 42, "L"), (opp, 52, "L")
338
+ ], i)
249
339
 
250
- pdf.ln(6)
251
-
252
- pdf.sub_title("4b. Customers Without Sophos Firewall", SOPHOS_BLUE)
340
+ # ===== SECTION 6: UPSELL - NO FIREWALL ====================================
341
+ pdf.add_page()
342
+ pdf.section_title("5. Upsell: Customers Without Sophos Firewall (20+ Users)")
253
343
  pdf.body_text(
254
- "The following customers with 20+ users have no Sophos firewall (XGS, SF Virtual, or XG) "
255
- "in their billing. These are candidates for Sophos Firewall or SF Virtual deployment, "
256
- "especially those already on MDR Complete who would benefit from firewall-endpoint integration."
344
+ "Customers with 20 or more users and no Sophos firewall (XGS, SF Virtual, or XG) "
345
+ "in their billing. Sorted by monthly spend. Those already on MDR Complete "
346
+ "would benefit most from firewall-endpoint integration via Synchronized Security."
257
347
  )
258
348
 
259
349
  no_fw = [
260
- ("Sydwest Multicultural Services", 114, 5, "$1,253.48", "MDR Complete, 119 assets. Large estate with NO firewall - top priority."),
261
- ("Master Builders Assoc NSW", 94, 11, "$1,163.00", "MDR Complete, 105 assets. Major opportunity for XGS deployment."),
262
- ("Martin Moris & Jones", 79, 2, "$967.30", "MDR Complete. 81 assets, no firewall."),
263
- ("QCC", 82, 2, "$871.58", "MDR Complete. 84 assets, no network protection."),
264
- ("Ingenuity Design Group", 67, 17, "$557.20", "MDR Essentials, 84 assets. Also upgrade path to MDR Complete."),
265
- ("Community Gateway", 54, 2, "$586.82", "MDR Complete. 56 assets, no firewall."),
266
- ("Macarthur Diversity Services", 42, 0, "$427.14", "MDR Complete. 42 users, no firewall, no servers."),
267
- ("Arrow Electrical Services", 54, 0, "$408.20", "MDR Complete + ZTNA. No firewall and no servers - check if needed."),
350
+ ("Sydwest Multicultural Services", 114, 5, "$1,253.48", "Complete", "TOP PRIORITY - largest no-FW customer"),
351
+ ("Master Builders Assoc NSW", 94, 11, "$1,163.00", "Complete", "105 assets with no network protection"),
352
+ ("ReadiNow Corporation", 58, 66, "$1,121.98", "Essentials", "124 assets - also MDR upgrade candidate"),
353
+ ("Martin Moris & Jones", 79, 2, "$967.30", "Complete", "81 assets no firewall"),
354
+ ("QCC", 82, 2, "$871.58", "Complete", "84 assets no network protection"),
355
+ ("Community Gateway", 54, 2, "$586.82", "Complete", "56 assets"),
356
+ ("Ingenuity Design Group", 67, 17, "$557.20", "Essentials", "84 assets - also MDR upgrade candidate"),
357
+ ("Macarthur Diversity Services", 42, 0, "$427.14", "Complete", "No FW, no servers"),
358
+ ("Atlas Engineering Group", 30, 1, "$323.92", "Complete", "31 assets"),
359
+ ("Regupol", 26, 3, "$320.88", "Complete", "29 assets"),
360
+ ("Gordon MacDonald", 31, 0, "$294.45", "Complete", "31 users"),
361
+ ("MMJ Real Estate", 23, 0, "$233.91", "Complete", "23 users"),
362
+ ("QE Innovations", 19, 1, "$212.05", "Complete", "20 assets"),
363
+ ("IAP2 Australasia", 20, 0, "$203.40", "Complete", "20 users"),
364
+ ("Market St", 19, 0, "$193.23", "Complete", "19 users"),
365
+ ("Southern Pumping", 18, 0, "$183.06", "Complete", "18 users"),
366
+ ("Andrew Webb Accounting", 18, 0, "$183.06", "Complete", "18 users"),
367
+ ("Anisimoff Legal", 17, 0, "$172.89", "Complete", "17 users"),
268
368
  ]
269
369
 
270
- cols = [("Customer", 52), ("Users", 14), ("Servers", 14), ("Current $", 22), ("Opportunity", 88)]
271
- pdf.table_header(cols)
272
- for i, (cust, users, servers, rev, notes) in enumerate(no_fw):
273
- pdf.table_row([(cust, 52, "L"), (str(users), 14, "C"), (str(servers), 14, "C"), (rev, 22, "R"), (notes, 88, "L")], i)
370
+ cols_def = [("Customer", 52), ("Users", 14), ("Svrs", 14), ("Monthly $", 22), ("MDR Tier", 22), ("Opportunity", 66)]
371
+ pdf.table_header(cols_def)
372
+ for i, (cust, users, servers, rev, mdr, opp) in enumerate(no_fw):
373
+ pdf.table_row([
374
+ (cust, 52, "L"), (str(users), 14, "C"), (str(servers), 14, "C"),
375
+ (rev, 22, "R"), (mdr, 22, "C"), (opp, 66, "L")
376
+ ], i)
274
377
 
275
- pdf.ln(6)
276
-
277
- pdf.sub_title("4c. MDR Essentials to Complete Upgrade Path", SOPHOS_BLUE)
378
+ # ===== SECTION 7: MDR ESSENTIALS TO COMPLETE ===============================
379
+ pdf.add_page()
380
+ pdf.section_title("6. MDR Essentials to Complete Upgrade Path")
278
381
  pdf.body_text(
279
- "These customers have MDR Essentials and could be upgraded to MDR Complete for full "
280
- "24/7 managed threat response. MDR Complete adds human-led threat hunting, incident "
281
- "response, and a dedicated response team."
382
+ "These customers currently have MDR Essentials and are candidates for upgrade to "
383
+ "MDR Complete. MDR Complete adds human-led threat hunting, full incident response, "
384
+ "and a dedicated Sophos response team. Sorted by monthly spend."
282
385
  )
283
386
 
284
- essentials_upgrade = [
285
- ("ReadiNow Corporation", 58, 66, "$1,121.98", "124 assets on Essentials. Largest Essentials customer - high value upgrade."),
286
- ("Moduplay Group", 125, 2, "$816.40", "127 assets on Essentials + 3 firewalls. Large estate."),
287
- ("Ingenuity Design Group", 67, 17, "$557.20", "84 assets on Essentials. Also no firewall."),
288
- ("National Justice Project", 29, 0, "$149.64", "29 users on Essentials."),
387
+ essentials = [
388
+ ("ReadiNow Corporation", 58, 66, "$1,121.98", "124 assets on Essentials - largest upgrade opportunity"),
389
+ ("Moduplay Group", 125, 2, "$816.40", "127 assets + 3 firewalls"),
390
+ ("Ingenuity Design Group", 67, 17, "$557.20", "84 assets - also no firewall"),
391
+ ("National Justice Project", 29, 0, "$149.64", "29 users on Essentials"),
289
392
  ]
290
393
 
291
- cols = [("Customer", 52), ("Users", 14), ("Servers", 14), ("Current $", 22), ("Notes", 88)]
292
- pdf.table_header(cols)
293
- for i, (cust, users, servers, rev, notes) in enumerate(essentials_upgrade):
294
- pdf.table_row([(cust, 52, "L"), (str(users), 14, "C"), (str(servers), 14, "C"), (rev, 22, "R"), (notes, 88, "L")], i)
394
+ cols_def = [("Customer", 52), ("Users", 16), ("Servers", 16), ("Monthly $", 24), ("Notes", 82)]
395
+ pdf.table_header(cols_def)
396
+ for i, (cust, users, servers, rev, notes) in enumerate(essentials):
397
+ pdf.table_row([
398
+ (cust, 52, "L"), (str(users), 16, "C"), (str(servers), 16, "C"),
399
+ (rev, 24, "R"), (notes, 82, "L")
400
+ ], i)
295
401
 
296
- # ═══════════════════════════════════════════════════════════════════════════════
297
- # SECTION 5: NO SERVER PROTECTION
298
- # ═══════════════════════════════════════════════════════════════════════════════
299
- pdf.add_page()
300
- pdf.section_title("5. No Server Protection Enrolled")
402
+ pdf.ln(8)
403
+
404
+ # ===== SECTION 8: NO SERVER PROTECTION =====================================
405
+ pdf.section_title("7. No Server Protection Enrolled (14 Tenants)")
301
406
  pdf.body_text(
302
- "14 tenants have no server protection enrolled in Sophos Central. These customers either "
303
- "have no servers or their servers are unprotected. For customers with on-premises or "
304
- "cloud infrastructure, server protection is essential."
407
+ "These tenants have no server protection enrolled in Sophos Central. For customers "
408
+ "with on-premises or cloud servers, Sophos Server Protection or MDR Server should "
409
+ "be deployed. Some may genuinely have no servers (SaaS-only organisations)."
305
410
  )
306
411
 
307
412
  no_server = [
308
- "Big Fat Smile", "Intrec Management Pty Ltd", "ROKT Pty Limited",
309
- "Golden West Holden", "Etheridge Ford", "Gateway Motor Group",
310
- "C-Lite Pty Ltd", "C-Lite", "True Blue Chemicals",
311
- "Sydney Auto Paint", "CHL Wollongong Pty Limited", "Pumps United",
312
- "Seapost Pty Ltd t/as Dion's Bus Service", "Kiama Community College",
413
+ "Big Fat Smile",
414
+ "Intrec Management Pty Ltd",
415
+ "ROKT Pty Limited",
416
+ "Golden West Holden",
417
+ "Etheridge Ford",
418
+ "Gateway Motor Group",
419
+ "C-Lite Pty Ltd",
420
+ "C-Lite",
421
+ "True Blue Chemicals",
422
+ "Sydney Auto Paint",
423
+ "CHL Wollongong Pty Limited",
424
+ "Pumps United",
425
+ "Seapost Pty Ltd t/as Dion's Bus Service",
426
+ "Kiama Community College",
313
427
  ]
314
428
 
315
- cols = [("Customer", 95), ("Recommended Action", 95)]
316
- pdf.table_header(cols)
429
+ cols_def = [("Customer", 95), ("Recommended Action", 95)]
430
+ pdf.table_header(cols_def)
317
431
  for i, cust in enumerate(no_server):
318
432
  action = "Assess server estate; deploy Sophos Server Protection or MDR Server"
319
433
  pdf.table_row([(cust, 95, "L"), (action, 95, "L")], i)
320
434
 
321
- # ═══════════════════════════════════════════════════════════════════════════════
322
- # SECTION 6: ACTION PLAN
323
- # ═══════════════════════════════════════════════════════════════════════════════
324
- pdf.ln(8)
325
- pdf.section_title("6. Recommended Action Plan")
435
+ # ===== SECTION 9: ACTION PLAN =============================================
436
+ pdf.add_page()
437
+ pdf.section_title("8. Recommended Action Plan")
438
+
439
+ # Immediate
440
+ pdf.sub_title("Immediate - This Week", SOPHOS_RED)
441
+ imm = [
442
+ ("Fix Critical TP Gaps", "Re-enable tamper protection on 4 tenants at 0%: Marco Polo Aged Care, Unibuild, MasterCraft Windows and Doors, Martin Morris & Jones Sydney"),
443
+ ("Investigate WIN Sports", "2 enrolled servers showing 0% protection - determine root cause"),
444
+ ("Fix Durham Green", "TP at 59% - multiple endpoints need TP re-enabled"),
445
+ ]
446
+ cols_def = [("Action", 50), ("Detail", 140)]
447
+ pdf.table_header(cols_def)
448
+ for i, (action, detail) in enumerate(imm):
449
+ pdf.table_row([(action, 50, "L"), (detail, 140, "L")], i)
326
450
 
327
- pdf.sub_title("Immediate (This Week)", SOPHOS_RED)
328
- pdf.body_text(
329
- "1. Re-enable tamper protection on the 4 critical tenants (Marco Polo Aged Care, "
330
- "Unibuild, MasterCraft, Martin Morris & Jones Sydney)\n"
331
- "2. Investigate WIN Sports server protection at 0%\n"
332
- "3. Fix Durham Green (59% TP) - multiple endpoints need TP re-enabled"
333
- )
451
+ pdf.ln(6)
334
452
 
335
- pdf.sub_title("Short Term (This Month)", SOPHOS_AMBER)
336
- pdf.body_text(
337
- "4. Remediate all server TP gaps (Southern Truck, Gunderson Briggs, IOH, PPM, Sydney Auto Paints)\n"
338
- "5. Clean up moderate TP gaps (10 tenants with 92-99% scores - likely stale devices)\n"
339
- "6. Begin conversations with top MDR upsell targets: Southern Truck Centre (41 users), "
340
- "Gunderson Briggs (25 assets)"
341
- )
453
+ # Short term
454
+ pdf.sub_title("Short Term - This Month", SOPHOS_AMBER)
455
+ short = [
456
+ ("Fix Server TP Gaps", "Southern Truck Centre, Gunderson Briggs, IOH, Project Portfolio Management, Sydney Automotive Paints & Equipment"),
457
+ ("Clean Up Moderate Gaps", "10 tenants with 92-99% TP scores - likely stale devices to remove or re-enable"),
458
+ ("Begin MDR Conversations", "Top targets: Southern Truck Centre (43 assets), Gunderson Briggs (25 assets), Tulich Project Management (servers with no MDR)"),
459
+ ]
460
+ cols_def = [("Action", 50), ("Detail", 140)]
461
+ pdf.table_header(cols_def)
462
+ for i, (action, detail) in enumerate(short):
463
+ pdf.table_row([(action, 50, "L"), (detail, 140, "L")], i)
342
464
 
343
- pdf.sub_title("Quarterly Business Review Conversations", SOPHOS_BLUE)
344
- pdf.body_text(
345
- "7. Firewall opportunities: Sydwest Multicultural (114 users, no FW), Master Builders (105 assets, no FW), "
346
- "QCC (84 assets, no FW)\n"
347
- "8. MDR Complete upgrades: ReadiNow (124 assets), Moduplay (127 assets), Ingenuity Design (84 assets)\n"
348
- "9. Server protection for the 14 tenants with no server enrollment\n"
349
- "10. Review all 142 billing customers against the 153 tenants - 11 tenants have no billing "
350
- "(may be trial, internal, or decommissioned)"
351
- )
465
+ pdf.ln(6)
352
466
 
467
+ # QBR
468
+ pdf.sub_title("QBR Conversations", SOPHOS_BLUE)
469
+ qbr = [
470
+ ("Firewall Opportunities", "Sydwest Multicultural (114 users), Master Builders (94 users), QCC (82 users) - all MDR Complete with no firewall"),
471
+ ("MDR Complete Upgrades", "ReadiNow (124 assets), Moduplay (127 assets), Ingenuity Design (84 assets) - all on MDR Essentials"),
472
+ ("Server Protection", "14 tenants with no server enrolment - assess server estates and deploy protection"),
473
+ ]
474
+ cols_def = [("Action", 50), ("Detail", 140)]
475
+ pdf.table_header(cols_def)
476
+ for i, (action, detail) in enumerate(qbr):
477
+ pdf.table_row([(action, 50, "L"), (detail, 140, "L")], i)
478
+
479
+ pdf.ln(10)
480
+
481
+ # Data source note
482
+ pdf.set_draw_color(*SOPHOS_BLUE)
483
+ pdf.set_line_width(0.3)
484
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
353
485
  pdf.ln(4)
354
- pdf.set_font("Helvetica", "I", 9)
486
+ pdf.set_font("Helvetica", "I", 8)
355
487
  pdf.set_text_color(*SOPHOS_GREY)
356
- pdf.multi_cell(0, 5, (
357
- "Data sourced from Sophos Central API via MCP Server v0.2.6. "
488
+ pdf.multi_cell(0, 4.5, (
489
+ "Data sourced from Sophos Central API via MCP Server v0.2.4. "
358
490
  "Health data from Account Health Check API. Billing data from Partner Billing Usage API (March 2026). "
359
491
  "All monetary values in AUD at partner net pricing."
360
492
  ))
361
493
 
362
- # ── Save ──
494
+ # == Save ==================================================================
363
495
  out_dir = r"C:\Users\Aaron.Jacobs\OneDrive - Sophos Ltd\projects"
364
496
  out_path = os.path.join(out_dir, "Rodin_Sophos_Estate_Health_Report_March_2026.pdf")
365
497
  pdf.output(out_path)
366
498
  print(f"Report saved to: {out_path}")
499
+ print(f"Pages: {pdf.page_no()}")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sophos-central-mcp-server",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "MCP server for Sophos Central API - SOC monitoring, endpoint management, admin automation, and XDR investigation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",