def _perform_http_request_internal(self, url: str, req: Request): opener: Optional[OpenerDirector] = None # for security (BAN-B310) if url.lower().startswith("http"): if self.proxy is not None: if isinstance(self.proxy, str): opener = urllib.request.build_opener( ProxyHandler({"http": self.proxy, "https": self.proxy}), HTTPSHandler(context=self.ssl), ) else: raise SlackRequestError( f"Invalid proxy detected: {self.proxy} must be a str value" ) else: raise SlackRequestError(f"Invalid URL detected: {url}") # NOTE: BAN-B310 is already checked above http_resp: Optional[HTTPResponse] = None if opener: http_resp = opener.open(req, timeout=self.timeout) # skipcq: BAN-B310 else: http_resp = urlopen( # skipcq: BAN-B310 req, context=self.ssl, timeout=self.timeout ) charset: str = http_resp.headers.get_content_charset() or "utf-8" response_body: str = http_resp.read().decode(charset) resp = WebhookResponse( url=url, status_code=http_resp.status, body=response_body, headers=http_resp.headers, ) _debug_log_response(self.logger, resp) return resp
def _perform_urllib_http_request_internal( self, url: str, req: Request, ) -> Dict[str, Any]: # urllib not only opens http:// or https:// URLs, but also ftp:// and file://. # With this it might be possible to open local files on the executing machine # which might be a security risk if the URL to open can be manipulated by an external user. # (BAN-B310) if url.lower().startswith("http"): opener: Optional[OpenerDirector] = None if self.proxy is not None: if isinstance(self.proxy, str): opener = urllib.request.build_opener( ProxyHandler({ "http": self.proxy, "https": self.proxy }), HTTPSHandler(context=self.ssl), ) else: raise SlackRequestError( f"Invalid proxy detected: {self.proxy} must be a str value" ) # NOTE: BAN-B310 is already checked above resp: Optional[HTTPResponse] = None if opener: resp = opener.open(req, timeout=self.timeout) # skipcq: BAN-B310 else: resp = urlopen( # skipcq: BAN-B310 req, context=self.ssl, timeout=self.timeout) if resp.headers.get_content_type() == "application/gzip": # admin.analytics.getFile body: bytes = resp.read() if self._logger.level <= logging.DEBUG: self._logger.debug("Received the following response - " f"status: {resp.code}, " f"headers: {dict(resp.headers)}, " f"body: (binary)") return { "status": resp.code, "headers": resp.headers, "body": body } charset = resp.headers.get_content_charset() or "utf-8" body: str = resp.read().decode( charset) # read the response body here if self._logger.level <= logging.DEBUG: self._logger.debug("Received the following response - " f"status: {resp.code}, " f"headers: {dict(resp.headers)}, " f"body: {body}") return {"status": resp.code, "headers": resp.headers, "body": body} raise SlackRequestError(f"Invalid URL detected: {url}")
def execute(config: Configuration, output_all: bool, output_file: str, output_slack: bool): with open(output_file, "w", newline='') as f: writer = csv.DictWriter( f, fieldnames=["secret", "project", "repository", "file", "commit"]) writer.writeheader() for repo_info, repo_config, secrets in scan(config, output_all): for secret in secrets: row = { "secret": secret["line"].strip() or secret["file"], "project": repo_info.project, "repository": repo_info.name, "file": secret["file"], "commit": secret["commit"] } print(row) writer.writerow(row) if output_slack and repo_config.slack_webhook: webhook = WebhookClient(repo_config.slack_webhook) for blocks in SlackMessageBuilder(repo_info, secrets).build(): response = webhook.send(text="fallback", blocks=blocks) if response.status_code != 200: raise SlackRequestError( f"Error when sending message blocks to slack: {response.body}" )
def _build_req_args( *, token: Optional[str], http_verb: str, files: dict, data: dict, default_params: dict, params: dict, json: dict, # skipcq: PYL-W0621 headers: dict, auth: dict, ssl: Optional[SSLContext], proxy: Optional[str], ) -> dict: has_json = json is not None has_files = files is not None if has_json and http_verb != "POST": msg = "Json data can only be submitted as POST requests. GET requests should use the 'params' argument." raise SlackRequestError(msg) if data is not None and isinstance(data, dict): data = {k: v for k, v in data.items() if v is not None} _set_default_params(data, default_params) if files is not None and isinstance(files, dict): files = {k: v for k, v in files.items() if v is not None} # NOTE: We do not need to all #_set_default_params here # because other parameters in binary data requests can exist # only in either data or params, not in files. if params is not None and isinstance(params, dict): params = {k: v for k, v in params.items() if v is not None} _set_default_params(params, default_params) if json is not None and isinstance(json, dict): _set_default_params(json, default_params) token: Optional[str] = token if params is not None and "token" in params: token = params.pop("token") if json is not None and "token" in json: token = json.pop("token") req_args = { "headers": _get_headers( headers=headers, token=token, has_json=has_json, has_files=has_files, request_specific_headers=headers, ), "data": data, "files": files, "params": params, "json": json, "ssl": ssl, "proxy": proxy, "auth": auth, } return req_args
def _update_call_participants( kwargs, users: Union[str, Sequence[Dict[str, str]]] ) -> None: if users is None: return if isinstance(users, list): kwargs.update({"users": json.dumps(users)}) elif isinstance(users, str): kwargs.update({"users": users}) else: raise SlackRequestError("users must be either str or Sequence[Dict[str, str]]")
def _perform_http_request( self, *, http_verb: str = "GET", url: str, body_params: Optional[Dict[str, any]] = None, headers: Dict[str, str], ) -> AuditLogsResponse: if body_params is not None: body_params = json.dumps(body_params) headers["Content-Type"] = "application/json;charset=utf-8" if self.logger.level <= logging.DEBUG: headers_for_logging = { k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items() } self.logger.debug( f"Sending a request - url: {url}, body: {body_params}, headers: {headers_for_logging}" ) try: opener: Optional[OpenerDirector] = None # for security (BAN-B310) if url.lower().startswith("http"): req = Request( method=http_verb, url=url, data=body_params.encode("utf-8") if body_params is not None else None, headers=headers, ) if self.proxy is not None: if isinstance(self.proxy, str): opener = urllib.request.build_opener( ProxyHandler({ "http": self.proxy, "https": self.proxy }), HTTPSHandler(context=self.ssl), ) else: raise SlackRequestError( f"Invalid proxy detected: {self.proxy} must be a str value" ) else: raise SlackRequestError(f"Invalid URL detected: {url}") # NOTE: BAN-B310 is already checked above resp: Optional[HTTPResponse] = None if opener: resp = opener.open(req, timeout=self.timeout) # skipcq: BAN-B310 else: resp = urlopen( # skipcq: BAN-B310 req, context=self.ssl, timeout=self.timeout) charset: str = resp.headers.get_content_charset() or "utf-8" response_body: str = resp.read().decode(charset) resp = AuditLogsResponse( url=url, status_code=resp.status, raw_body=response_body, headers=resp.headers, ) _debug_log_response(self.logger, resp) return resp except HTTPError as e: # read the response body here charset = e.headers.get_content_charset() or "utf-8" body_params: str = e.read().decode(charset) resp = AuditLogsResponse( url=url, status_code=e.code, raw_body=body_params, headers=e.headers, ) if e.code == 429: # for backward-compatibility with WebClient (v.2.5.0 or older) resp.headers["Retry-After"] = resp.headers["retry-after"] _debug_log_response(self.logger, resp) return resp except Exception as err: self.logger.error( f"Failed to send a request to Slack API server: {err}") raise err
def _perform_urllib_http_request( self, *, url: str, args: Dict[str, Dict[str, any]]) -> Dict[str, any]: """Performs an HTTP request and parses the response. :param url: a complete URL (e.g., https://www.slack.com/api/chat.postMessage) :param args: args has "headers", "data", "params", and "json" "headers": Dict[str, str] "data": Dict[str, any] "params": Dict[str, str], "json": Dict[str, any], :return: dict {status: int, headers: Headers, body: str} """ headers = args["headers"] if args["json"]: body = json.dumps(args["json"]) headers["Content-Type"] = "application/json;charset=utf-8" elif args["data"]: boundary = f"--------------{uuid.uuid4()}" sep_boundary = b"\r\n--" + boundary.encode("ascii") end_boundary = sep_boundary + b"--\r\n" body = io.BytesIO() data = args["data"] for key, value in data.items(): readable = getattr(value, "readable", None) if readable and value.readable(): filename = "Uploaded file" name_attr = getattr(value, "name", None) if name_attr: filename = (name_attr.decode("utf-8") if isinstance( name_attr, bytes) else name_attr) if "filename" in data: filename = data["filename"] mimetype = (mimetypes.guess_type(filename)[0] or "application/octet-stream") title = ( f'\r\nContent-Disposition: form-data; name="{key}"; filename="{filename}"\r\n' + f"Content-Type: {mimetype}\r\n") value = value.read() else: title = f'\r\nContent-Disposition: form-data; name="{key}"\r\n' value = str(value).encode("utf-8") body.write(sep_boundary) body.write(title.encode("utf-8")) body.write(b"\r\n") body.write(value) body.write(end_boundary) body = body.getvalue() headers[ "Content-Type"] = f"multipart/form-data; boundary={boundary}" headers["Content-Length"] = len(body) elif args["params"]: body = urlencode(args["params"]) headers["Content-Type"] = "application/x-www-form-urlencoded" else: body = None if isinstance(body, str): body = body.encode("utf-8") # NOTE: Intentionally ignore the `http_verb` here # Slack APIs accepts any API method requests with POST methods try: # urllib not only opens http:// or https:// URLs, but also ftp:// and file://. # With this it might be possible to open local files on the executing machine # which might be a security risk if the URL to open can be manipulated by an external user. # (BAN-B310) if url.lower().startswith("http"): req = Request(method="POST", url=url, data=body, headers=headers) opener: Optional[OpenerDirector] = None if self.proxy is not None: if isinstance(self.proxy, str): opener = urllib.request.build_opener( ProxyHandler({ "http": self.proxy, "https": self.proxy }), HTTPSHandler(context=self.ssl), ) else: raise SlackRequestError( f"Invalid proxy detected: {self.proxy} must be a str value" ) # NOTE: BAN-B310 is already checked above resp: Optional[HTTPResponse] = None if opener: resp = opener.open( req, timeout=self.timeout) # skipcq: BAN-B310 else: resp = urlopen( # skipcq: BAN-B310 req, context=self.ssl, timeout=self.timeout) if resp.headers.get_content_type() == "application/gzip": # admin.analytics.getFile body: bytes = resp.read() return { "status": resp.code, "headers": resp.headers, "body": body } charset = resp.headers.get_content_charset() or "utf-8" body: str = resp.read().decode( charset) # read the response body here return { "status": resp.code, "headers": resp.headers, "body": body } raise SlackRequestError(f"Invalid URL detected: {url}") except HTTPError as e: resp = {"status": e.code, "headers": e.headers} if e.code == 429: # for compatibility with aiohttp resp["headers"]["Retry-After"] = resp["headers"]["retry-after"] # read the response body here charset = e.headers.get_content_charset() or "utf-8" body: str = e.read().decode(charset) resp["body"] = body return resp except Exception as err: self._logger.error( f"Failed to send a request to Slack API server: {err}") raise err
def _perform_http_request(self, *, body: Dict[str, any], headers: Dict[str, str]) -> WebhookResponse: """Performs an HTTP request and parses the response. :param url: a complete URL to send data (e.g., https://hooks.slack.com/XXX) :param body: request body data :param headers: complete set of request headers :return: API response """ body = json.dumps(body) headers["Content-Type"] = "application/json;charset=utf-8" if self.logger.level <= logging.DEBUG: self.logger.debug( f"Sending a request - url: {self.url}, body: {body}, headers: {headers}" ) try: url = self.url opener: Optional[OpenerDirector] = None # for security (BAN-B310) if url.lower().startswith("http"): req = Request(method="POST", url=url, data=body.encode("utf-8"), headers=headers) if self.proxy is not None: if isinstance(self.proxy, str): opener = urllib.request.build_opener( ProxyHandler({ "http": self.proxy, "https": self.proxy }), HTTPSHandler(context=self.ssl), ) else: raise SlackRequestError( f"Invalid proxy detected: {self.proxy} must be a str value" ) else: raise SlackRequestError(f"Invalid URL detected: {url}") # NOTE: BAN-B310 is already checked above resp: Optional[HTTPResponse] = None if opener: resp = opener.open(req, timeout=self.timeout) # skipcq: BAN-B310 else: resp = urlopen( # skipcq: BAN-B310 req, context=self.ssl, timeout=self.timeout) charset: str = resp.headers.get_content_charset() or "utf-8" response_body: str = resp.read().decode(charset) resp = WebhookResponse( url=url, status_code=resp.status, body=response_body, headers=resp.headers, ) _debug_log_response(self.logger, resp) return resp except HTTPError as e: # read the response body here charset = e.headers.get_content_charset() or "utf-8" body: str = e.read().decode(charset) resp = WebhookResponse( url=url, status_code=e.code, body=body, headers=e.headers, ) if e.code == 429: # for backward-compatibility with WebClient (v.2.5.0 or older) resp.headers["Retry-After"] = resp.headers["retry-after"] _debug_log_response(self.logger, resp) return resp except Exception as err: self.logger.error( f"Failed to send a request to Slack API server: {err}") raise err