cyfendo 2.0.0__tar.gz

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.
cyfendo-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: cyfendo
3
+ Version: 2.0.0
4
+ Summary: Lightweight, zero-dependency CLI scanner for the Cyfendo Autonomous Security Platform
5
+ Author: Cyfendo Security Team
6
+ Author-email: Cyfendo Security Team <contact@cyfendo.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://cyfendo.com
9
+ Project-URL: Documentation, https://cyfendo.com/docs
10
+ Project-URL: Developer Settings, https://cyfendo.com/settings/developer
11
+ Keywords: security,sast,vulnerability-scanner,appsec,cyfendo,patching
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Dynamic: author
24
+ Dynamic: requires-python
25
+
26
+ # Cyfendo CLI
27
+
28
+ > **Fast, zero-dependency command-line interface for Cyfendo Autonomous Security Platform.**
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ # Global pip install
34
+ pip install cyfendo
35
+
36
+ # Or install in editable/development mode
37
+ pip install -e ./cli
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ # 1. Authenticate with your Cyfendo API Key
44
+ cyfendo login --key cy_live_xxxxxxxxxxxxxxxx
45
+
46
+ # 2. Run a vulnerability scan in the current directory
47
+ cyfendo scan .
48
+
49
+ # 3. Target an existing protected project
50
+ cyfendo scan . --project "payment-gateway"
51
+
52
+ # 4. View and export individual AI remediation patches
53
+ cyfendo patches
54
+ cyfendo patch fnd_8b22 --show
55
+ cyfendo patch fnd_8b22 --apply
56
+
57
+ # 5. CI/CD automation with SARIF export and threshold gate
58
+ cyfendo scan . --fail-on=high --sarif=results.sarif --no-interactive
59
+ ```
@@ -0,0 +1,34 @@
1
+ # Cyfendo CLI
2
+
3
+ > **Fast, zero-dependency command-line interface for Cyfendo Autonomous Security Platform.**
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Global pip install
9
+ pip install cyfendo
10
+
11
+ # Or install in editable/development mode
12
+ pip install -e ./cli
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # 1. Authenticate with your Cyfendo API Key
19
+ cyfendo login --key cy_live_xxxxxxxxxxxxxxxx
20
+
21
+ # 2. Run a vulnerability scan in the current directory
22
+ cyfendo scan .
23
+
24
+ # 3. Target an existing protected project
25
+ cyfendo scan . --project "payment-gateway"
26
+
27
+ # 4. View and export individual AI remediation patches
28
+ cyfendo patches
29
+ cyfendo patch fnd_8b22 --show
30
+ cyfendo patch fnd_8b22 --apply
31
+
32
+ # 5. CI/CD automation with SARIF export and threshold gate
33
+ cyfendo scan . --fail-on=high --sarif=results.sarif --no-interactive
34
+ ```
@@ -0,0 +1,7 @@
1
+ """
2
+ Cyfendo CLI - Lightweight Local Scanner
3
+ Fast, zero-dependency command-line interface for the Cyfendo Autonomous Security Platform.
4
+ """
5
+
6
+ __version__ = "2.0.0"
7
+ __author__ = "Cyfendo Security Team"
@@ -0,0 +1,218 @@
1
+ """
2
+ Cyfendo API Client Module
3
+ Lightweight HTTP client built entirely with Python standard libraries (urllib).
4
+ Handles API Key authentication, multipart file uploads, and streaming endpoints.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import uuid
10
+ import urllib.request
11
+ import urllib.parse
12
+ import urllib.error
13
+ import ssl
14
+ from pathlib import Path
15
+ from typing import Optional, Dict, Any, Tuple
16
+ try:
17
+ from . import __version__
18
+ except ImportError:
19
+ from cyfendo import __version__
20
+
21
+
22
+ DEFAULT_SERVER_URL = os.environ.get("CYFENDO_SERVER_URL", "https://cyfendo.com")
23
+ CONFIG_DIR = Path.home() / ".cyfendo"
24
+ CONFIG_FILE = CONFIG_DIR / "config.json"
25
+
26
+
27
+ class CyfendoClientError(Exception):
28
+ """Base exception for Cyfendo API client errors."""
29
+ def __init__(self, message: str, status_code: Optional[int] = None, response_data: Optional[dict] = None):
30
+ super().__init__(message)
31
+ self.status_code = status_code
32
+ self.response_data = response_data or {}
33
+
34
+
35
+ class CyfendoClient:
36
+ def __init__(self, api_key: Optional[str] = None, server_url: Optional[str] = None):
37
+ self.config = self.load_config()
38
+ self.api_key = api_key or os.environ.get("CYFENDO_API_KEY") or self.config.get("api_key")
39
+ self.server_url = (server_url or os.environ.get("CYFENDO_SERVER_URL") or self.config.get("server_url") or DEFAULT_SERVER_URL).rstrip("/")
40
+
41
+ @staticmethod
42
+ def load_config() -> Dict[str, Any]:
43
+ """Loads credentials and settings from ~/.cyfendo/config.json."""
44
+ if CONFIG_FILE.is_file():
45
+ try:
46
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
47
+ return json.load(f)
48
+ except Exception:
49
+ return {}
50
+ return {}
51
+
52
+ @classmethod
53
+ def save_config(cls, api_key: str, server_url: Optional[str] = None) -> None:
54
+ """Saves authentication credentials to ~/.cyfendo/config.json."""
55
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
56
+ config_data = cls.load_config()
57
+ config_data["api_key"] = api_key.strip()
58
+ if server_url:
59
+ config_data["server_url"] = server_url.rstrip("/")
60
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
61
+ json.dump(config_data, f, indent=2)
62
+ os.chmod(CONFIG_FILE, 0o600)
63
+
64
+ @classmethod
65
+ def clear_config(cls) -> None:
66
+ """Clears local credentials."""
67
+ if CONFIG_FILE.is_file():
68
+ try:
69
+ os.remove(CONFIG_FILE)
70
+ except Exception:
71
+ pass
72
+
73
+ def _get_headers(self, content_type: Optional[str] = None) -> Dict[str, str]:
74
+ """Builds default request headers with Bearer API Key."""
75
+ headers = {
76
+ "User-Agent": f"Cyfendo-CLI/{__version__}",
77
+ "Accept": "application/json"
78
+ }
79
+ if self.api_key:
80
+ headers["Authorization"] = f"Bearer {self.api_key}"
81
+ headers["X-Cyfendo-Api-Key"] = self.api_key
82
+ if content_type:
83
+ headers["Content-Type"] = content_type
84
+ return headers
85
+
86
+ def _make_request(
87
+ self,
88
+ path: str,
89
+ method: str = "GET",
90
+ data: Optional[bytes] = None,
91
+ headers: Optional[Dict[str, str]] = None,
92
+ timeout: int = 60
93
+ ) -> Tuple[int, Any]:
94
+ """Executes HTTP request via standard library urllib."""
95
+ url = f"{self.server_url}{path}" if path.startswith("/") else f"{self.server_url}/{path}"
96
+ req_headers = self._get_headers()
97
+ if headers:
98
+ req_headers.update(headers)
99
+
100
+ req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
101
+ ctx = ssl.create_default_context()
102
+
103
+ try:
104
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as response:
105
+ status_code = response.getcode()
106
+ content_type = response.headers.get("Content-Type", "")
107
+ raw_bytes = response.read()
108
+
109
+ if "application/json" in content_type:
110
+ return status_code, json.loads(raw_bytes.decode("utf-8"))
111
+ return status_code, raw_bytes
112
+ except urllib.error.HTTPError as e:
113
+ err_body = e.read().decode("utf-8", errors="replace")
114
+ err_json = {}
115
+ try:
116
+ err_json = json.loads(err_body)
117
+ err_msg = err_json.get("error") or err_json.get("message") or str(e)
118
+ except Exception:
119
+ err_msg = err_body[:200] if err_body else str(e)
120
+ raise CyfendoClientError(err_msg, status_code=e.code, response_data=err_json)
121
+ except urllib.error.URLError as e:
122
+ raise CyfendoClientError(f"Connection failed to {self.server_url}: {e.reason}")
123
+ except Exception as e:
124
+ raise CyfendoClientError(f"Unexpected network error: {str(e)}")
125
+
126
+ def whoami(self) -> Dict[str, Any]:
127
+ """Retrieves authenticated user identity and workspace status."""
128
+ _, data = self._make_request("/api/v1/cli/whoami")
129
+ return data
130
+
131
+ def list_projects(self) -> Dict[str, Any]:
132
+ """Retrieves list of active protected projects in workspace."""
133
+ _, data = self._make_request("/api/v1/cli/status")
134
+ return data
135
+
136
+ def submit_scan(
137
+ self,
138
+ zip_bytes: bytes,
139
+ project_name: str,
140
+ project_id: Optional[str] = None,
141
+ scan_profile: str = "deep_agentic",
142
+ confirm_separate: bool = False,
143
+ generate_patches: bool = False,
144
+ dedup_scope: str = "root_cause",
145
+ benchmark_mode: bool = False
146
+ ) -> Dict[str, Any]:
147
+ """
148
+ Submits workspace archive to the single-shot CLI scan endpoint.
149
+ Uses pure standard library multipart/form-data encoding.
150
+ """
151
+ boundary = f"----CyfendoBoundary{uuid.uuid4().hex}"
152
+ body_parts = []
153
+
154
+ # Form fields
155
+ fields = {
156
+ "repo_name": project_name,
157
+ "scan_profile": scan_profile,
158
+ "confirm_separate": "true" if confirm_separate else "false",
159
+ "generate_patches": "true" if generate_patches else "false",
160
+ "dedup_scope": str(dedup_scope or "root_cause"),
161
+ "benchmark_mode": "true" if benchmark_mode else "false"
162
+ }
163
+ if project_id:
164
+ fields["project_id"] = project_id
165
+
166
+ for k, v in fields.items():
167
+ body_parts.append(f"--{boundary}\r\n".encode("utf-8"))
168
+ body_parts.append(f'Content-Disposition: form-data; name="{k}"\r\n\r\n'.encode("utf-8"))
169
+ body_parts.append(f"{v}\r\n".encode("utf-8"))
170
+
171
+ # File part
172
+ body_parts.append(f"--{boundary}\r\n".encode("utf-8"))
173
+ body_parts.append(f'Content-Disposition: form-data; name="file"; filename="source.zip"\r\n'.encode("utf-8"))
174
+ body_parts.append(b"Content-Type: application/zip\r\n\r\n")
175
+ body_parts.append(zip_bytes)
176
+ body_parts.append(b"\r\n")
177
+ body_parts.append(f"--{boundary}--\r\n".encode("utf-8"))
178
+
179
+ payload = b"".join(body_parts)
180
+ content_type = f"multipart/form-data; boundary={boundary}"
181
+
182
+ _, data = self._make_request(
183
+ "/api/v1/cli/scan",
184
+ method="POST",
185
+ data=payload,
186
+ headers={"Content-Type": content_type},
187
+ timeout=120
188
+ )
189
+ return data
190
+
191
+ def get_scan(self, scan_id: str) -> Dict[str, Any]:
192
+ """Retrieves scan details, summary, and findings."""
193
+ _, data = self._make_request(f"/api/v1/scans/{scan_id}")
194
+ return data
195
+
196
+ def get_patches(self, scan_id: str) -> Dict[str, Any]:
197
+ """Retrieves all finding patches for scan."""
198
+ _, data = self._make_request(f"/api/v1/scans/{scan_id}/patches")
199
+ return data
200
+
201
+ def get_sarif(self, scan_id: str) -> Dict[str, Any]:
202
+ """Retrieves standard SARIF 2.1.0 report."""
203
+ _, data = self._make_request(f"/api/v1/scans/{scan_id}/export/sarif")
204
+ return data
205
+
206
+ def get_markdown_report(self, scan_id: str) -> str:
207
+ """Retrieves full Markdown audit report text."""
208
+ _, data = self._make_request(f"/api/v1/scans/{scan_id}/export/markdown")
209
+ if isinstance(data, bytes):
210
+ return data.decode("utf-8", errors="replace")
211
+ return str(data)
212
+
213
+ def get_unified_patch(self, scan_id: str) -> str:
214
+ """Retrieves aggregated unified diff patch file."""
215
+ _, data = self._make_request(f"/api/v1/scans/{scan_id}/export/patch")
216
+ if isinstance(data, bytes):
217
+ return data.decode("utf-8", errors="replace")
218
+ return str(data)
@@ -0,0 +1,81 @@
1
+ """
2
+ Live Telemetry & SSE Streaming Module
3
+ Connects to Cyfendo Server-Sent Events (SSE) endpoint to stream live multi-agent scanning progress.
4
+ """
5
+
6
+ import json
7
+ import time
8
+ import urllib.request
9
+ import ssl
10
+ from typing import Callable, Optional, Dict, Any
11
+
12
+
13
+ class SSEStreamError(Exception):
14
+ pass
15
+
16
+
17
+ def stream_scan_events(
18
+ server_url: str,
19
+ scan_id: str,
20
+ api_key: Optional[str] = None,
21
+ on_event: Optional[Callable[[Dict[str, Any]], None]] = None,
22
+ timeout: int = 300
23
+ ) -> Dict[str, Any]:
24
+ """
25
+ Subscribes to /api/v1/scans/<scan_id>/events and yields live telemetry events.
26
+ Returns the final completion event payload.
27
+ """
28
+ url = f"{server_url.rstrip('/')}/api/v1/scans/{scan_id}/events"
29
+ headers = {
30
+ "Accept": "text/event-stream",
31
+ "Cache-Control": "no-cache",
32
+ "User-Agent": "Cyfendo-CLI/2.0.0"
33
+ }
34
+ if api_key:
35
+ headers["Authorization"] = f"Bearer {api_key}"
36
+ headers["X-Cyfendo-Api-Key"] = api_key
37
+
38
+ req = urllib.request.Request(url, headers=headers, method="GET")
39
+ ctx = ssl.create_default_context()
40
+
41
+ start_time = time.time()
42
+ last_event: Dict[str, Any] = {"stage": "running", "progress_percentage": 0}
43
+
44
+ try:
45
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as response:
46
+ buffer = ""
47
+ for raw_chunk in response:
48
+ if time.time() - start_time > timeout:
49
+ raise SSEStreamError(f"Scan telemetry stream timed out after {timeout} seconds.")
50
+
51
+ chunk = raw_chunk.decode("utf-8", errors="replace")
52
+ buffer += chunk
53
+
54
+ while "\n\n" in buffer:
55
+ message_block, buffer = buffer.split("\n\n", 1)
56
+ lines = message_block.split("\n")
57
+
58
+ event_data = None
59
+ for line in lines:
60
+ line = line.strip()
61
+ if line.startswith("data:"):
62
+ raw_data = line[5:].strip()
63
+ try:
64
+ event_data = json.loads(raw_data)
65
+ except Exception:
66
+ event_data = {"message": raw_data}
67
+
68
+ if event_data:
69
+ last_event = event_data
70
+ if on_event:
71
+ on_event(event_data)
72
+
73
+ stage = event_data.get("stage", "")
74
+ if stage in ("completed", "failed", "cancelled"):
75
+ return last_event
76
+
77
+ except (urllib.error.URLError, OSError) as e:
78
+ # If SSE stream drops, return last observed state
79
+ return last_event
80
+
81
+ return last_event