From f10f28d1002d0f4a5d3560b6a21f64e5da868e9b Mon Sep 17 00:00:00 2001 From: Antoine Drochon Date: Mon, 4 Mar 2024 16:16:06 -0800 Subject: [PATCH] v0.6.5 (#37) - Reorganize documentation into sub-pages - Add the full EAA Application Configuration with `include()` statements and included files - Introduce connector outbound allowlist to configure Firewall/Network Security Equipment where EAA Connector are operating (experimental) `akamai eaa connector allowlist` --- .gitignore | 3 +- README.md | 233 +--------------- bin/akamai-eaa | 43 ++- bin/config.py | 19 +- docs/commands/akamai-eaa-app.md | 260 ++++++++++++++++++ docs/commands/akamai-eaa-certificate.md | 47 ++++ docs/commands/akamai-eaa-connector.md | 110 ++++++++ docs/commands/akamai-eaa-dir.md | 35 +++ docs/commands/akamai-eaa-dp.md | 25 ++ docs/commands/akamai-eaa-log.md | 65 +++++ .../includes/akdemo-amer-2maincon.json | 12 + docs/examples/includes/akdemo-directory.json | 8 + .../includes/akdemo-groups-default.json | 27 ++ docs/examples/includes/akdemo-idp.json | 8 + libeaa/application.py | 106 +++++-- libeaa/common.py | 12 +- libeaa/connector.py | 46 ++++ libeaa/idp.py | 18 ++ requirements.txt | 2 + test/test.py | 48 +++- 20 files changed, 863 insertions(+), 264 deletions(-) create mode 100644 docs/commands/akamai-eaa-app.md create mode 100644 docs/commands/akamai-eaa-certificate.md create mode 100644 docs/commands/akamai-eaa-connector.md create mode 100644 docs/commands/akamai-eaa-dir.md create mode 100644 docs/commands/akamai-eaa-dp.md create mode 100644 docs/commands/akamai-eaa-log.md create mode 100644 docs/examples/includes/akdemo-amer-2maincon.json create mode 100644 docs/examples/includes/akdemo-directory.json create mode 100644 docs/examples/includes/akdemo-groups-default.json create mode 100644 docs/examples/includes/akdemo-idp.json diff --git a/.gitignore b/.gitignore index 6d460cc..f035007 100755 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ __pycache__ .vscode *.swp tags -docs/examples/includes/* +docs/examples/includes.akdemo/* +*.env diff --git a/README.md b/README.md index 2ef6792..eb61e44 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,11 @@ - [Key features](#key-features) - [Installation / upgrade](#installation--upgrade) - [Examples](#examples) - - [EAA event logs](#eaa-event-logs) + - [EAA Event Logs](#eaa-event-logs) - [Applications](#applications) - [Directory operations](#directory-operations) - [Connectors](#connectors) - - [Swapping connectors](#swapping-connectors) - [Certificate management](#certificate-management) - - [Display certificates](#display-certificates) - - [Rotation](#rotation) - [Device Posture Inventory](#device-posture-inventory) - [Known Limitations](#known-limitations) - [Troubleshooting and Support](#troubleshooting-and-support) @@ -67,241 +64,27 @@ See [install.md](docs/install.md) ### EAA Event Logs -EAA comes with two types of logs, the user access logs and the administrator audit logs. -For detailed description for each event field, refer to the product documentation on [https://techdocs.akamai.com](https://techdocs.akamai.com/eaa/docs/data-feed-siem). - -You can retrieve EAA events one of these ways: -- in near realtime using the argument `-f` or `--tail`. If you set `-f` and date range, the `-f` option will be ignored. -- retrieve a period of time based on EPOCH timestamp in `--start` and `--end` -- tune the acceptable delay vs. completeness with `--delay`. We recommend 10 minutes delay for full completeness. - -Pull user access logs, block until new logs are received. -You can stop using Control+C (Control+Break) or sending a SIG_INT or SIG_TERM signal to the process. - -```bash -$ akamai eaa log access --tail -``` - -You may want a one time chunk of log for a period of time, for example, the last 6 hours: - -```bash -$ START=$(bc <<< "$(date +%s) - 6 * 60 * 60") -$ akamai eaa log access -s $START -``` - -On Windows platforms, you can use PowerShell: -```powershell -PS /home/cli-eaa> $START = (Get-Date -UFormat %s) - 6 * 60 * 60 -PS /home/cli-eaa> akamai eaa log access -s $START -``` - -Send the **user access events** to a file (utf-8 encoding is being used): -```bash -$ akamai eaa log access --tail -o /tmp/eaa_access.log -``` - -Pull **admin audit events**, block till new logs are received -```bash -$ akamai eaa log admin --tail -``` +See [`akamai eaa log` documentation page](docs/commands/akamai-eaa-log.md) ### Applications -Common use cases: - -**Find an application**: -``` -$ akamai eaa search datascience -app://mD_Pw1XASpyVJc2JwgICTg,Data Science,akdemo-datascience,akdemo-datascience.go.akamai-access.com,4 -Found 1 app(s), total 124 app(s) -``` -Save an application: -You can save the application locally: -``` -$ akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg > ~/eaa_app_datascience_v3.json -``` - -**Restore the application**: -``` -$ cat ~/eaa_app_datascience_v3.json | akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg update -``` - -Or quickly walk through the JSON tree using `jq`. -``` -$ akamai eaa -b app app://mD_Pw1XASpyVJc2JwgICTg | jq .advanced_settings.websocket_enabled -"true" -``` - -**Delete an application**: -``` -akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg delete -``` - -Deploy an application, you can optionally add a comment to keep track of the change: -``` -akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg deploy --comment "[TICKET1234] Update service account credentials" -``` - -Finding an application using a specific connector name: -*What are the applications using connector `xyz`?*\ -Use `jq` and `grep`.\ -Note: we use `-b` to avoid the extra info the CLI spills out, like the footer. - -``` -$ akamai eaa -b search | akamai eaa app - | jq -j '.name, ": ", (.agents[]|.name, " "), "\n"'|grep xyz -``` - -View groups associated with a particular application: -``` -$ akamai eaa app app://FWbUCfpvRKaSOX1rl0u55Q viewgroups -``` - -You can pipe the command as well, for example to deploy all the application matching your search (eg. "bastion") - -``` -$ akamai eaa -b search bastion | akamai eaa app - deploy -``` - -Attach/detach connectors to a particular application: - -``` -$ akamai eaa app app://app-uuid-1 attach con://connector-uuid-1 con://connector-uuid-2 -$ akamai eaa app app://app-uuid-1 detach con://connector-uuid-1 con://connector-uuid-2 -``` - -### Directory operations - -**List the configured directories**: - -``` -$ akamai eaa dir -dir://FuiibQiDQzmC34oBx7INfQ,Cloud Directory,7 -dir://2Kz2YqmgSpqT_IJq9BLkWg,ad.akamaidemo.net,108 -dir://EX5-YjMyTrKgeWKHrqhUEA,Okta LDAP,10 -dir://Ygl1BpAFREiHrA8HR7dFhA,Azure AD,1 -``` -To view the output in JSON format and in follow mode to consume Directory Health: +See [`akamai eaa app` documentation page](docs/commands/akamai-eaa-app.md). -``` -$ akamai eaa dir list --json --tail | jq .name -"Cloud Directory" -"AD Domain AkamaiDemo.net (global)" -"Azure AD (Sync with SCIM)" -"AKDEMO AD with UPN" -``` - -**Trigger directory synchronization** +### Directory operations -``` -$ akamai eaa dir dir://2Kz2YqmgSpqT_IJq9BLkWg sync -Synchronize directory 2Kz2YqmgSpqT_IJq9BLkWg -Directory 2Kz2YqmgSpqT_IJq9BLkWg synchronization requested. -``` +See [`akamai eaa log` documentation page](docs/commands/akamai-eaa-dir.md) ### Connectors -Using the shortcut `c` and the `column` command available in most POSIX environment. -When piping, the extra information is written to *stderr* so they appear seperately. -This example shows a short command `akamai eaa c`, replacing `akamai eaa connector list`: - -``` -$ akamai eaa c | column -t -s, -Total 9 connector(s) -#Connector-id name reachable status version privateip publicip debug -con://cht3_GEjQWyMW9LEk7KQfg demo-v2-con-1-amer 1 1 4.4.0-2765 10.1.4.206 12.123.123.123 Y -con://Wy0Y6FrwQ66yQzLBAInC4w demo-v2-con-2-amer 1 1 4.4.0-2765 10.1.4.172 12.123.123.123 Y -con://dK0f1UvhR7i8-RByABDXaQ demo-v2-con-4-emea 1 1 4.4.0-2765 192.168.1.90 12.123.12.12 N -con://Ihmf51dASo-R1P37hzaP3Q demo-v2-con-3-emea 1 1 4.4.0-2765 192.168.1.235 12.123.12.12 N -con://XiCmu80xQcSWnaeQcvH8Vg demo-v2-con-5-apj 1 1 4.4.0-2765 192.168.1.228 12.123.123.12 Y -con://pkGjL5OgSjyHoymMguvp9Q demo-v2-con-6-apj 1 1 4.4.0-2765 192.168.1.144 12.123.123.12 Y -con://NAWSlptPSXOjq-bk2-EQPw demo-v2-con-10-rus 1 1 4.4.0-2765 10.3.0.101 12.123.123.12 Y -con://e_0nShZBQ7esNAC3ZEkhSQ demo-v2-con-3-amer 1 1 4.4.0-2765 10.1.4.83 12.123.123.123 Y -con://OEe9o-n2S_aMeZpLxgwG0A tmelab-sfo 1 1 4.4.0-2765 192.168.2.101 12.123.123.12 Y -``` - -To integrate connector health into your monitoring system, use the `--perf` option. -`akamai eaa c list --perf` -This provides 7 extra columns: -- CPU usage (%) -- Memory usage (%) -- Network Traffic (Mbps) -- Total of dialout connections -- Idle dialout connections -- Active dialout connections - -To correlate with applications served by each connector, use the `--showapps` argument to include a list of the application FQDNs as an array in the JSON response. - -#### Swapping connectors - -If you are doing a maintenance on an hypervizor, you may need to swap out 2 connectors. -The current implement look for all the apps, add the new connector, remove the old one. -The application is marked as ready to update. - -Caveats (let us know if you need it): -- This doesn't perform swap for directory -- There is no option to automatically redeploy the impacted application after the swap - -Example: -``` -$ akamai eaa connector con://e_0nShZBQ7esNAC3ZEkhSQ swap con://cht3_GEjQWyMW9LEk7KQfg -#Operation,connector-id,connector-name,app-id,app-name -+,con://cht3_GEjQWyMW9LEk7KQfg,demo-v2-con-1-amer,app://nSFDNGYARHeZGNlweIX7Wg,Speedtest (v2.1) --,con://e_0nShZBQ7esNAC3ZEkhSQ,demo-v2-con-3-amer,app://nSFDNGYARHeZGNlweIX7Wg,Speedtest (v2.1) -Connector swapped in 1 application(s). -Updated application(s) is/are marked as ready to deploy -``` +See [`akamai eaa connector` documentation page](docs/commands/akamai-eaa-connector.md). ### Certificate management -#### Display certificates - -The command `cert` displays all the certificate you have configured in EAA, along with the CN and SAN attribute in the `hosts` field, as a `+` separated list. - -Example with a wildcard certificate: - -``` -$ akamai eaa cert | head -n1 -#Certificate-ID,cn,type,expiration,days left,hosts -crt://KXi553saQSCeNI1_WH6xuA,*.akamaidemo.net,Custom,2031-06-05T22:56:34,3307,*.akamaidemo.net+akamaidemo.net -``` - -#### Rotate certificates - -The cli-eaa helps with this task with the `akamai eaa certificate` command. - -Pass the certificate and key file as parameter with the optional passphrase to replace the existing certificate. -By default, the rotation does NOT redeploy the impacted application or IdP. -To trigger the re-deployment of all impacted applications and IdP, add the ``--deployafter`` flag. - -``` -$ akamai eaa certificate crt://certificate-UUID rotate --key ~/certs/mycert.key --cert ~/certs/mycert.cert --deployafter -Rotating certificate certificate-UUID... -Certificate CN: *.akamaidemo.net (*.akamaidemo.net Lets Encrypt) -Certificate certificate-UUID updated, 3 application/IdP(s) have been marked ready for deployment. -Deploying application Multi-origin Active-Active Demo (US-East) (app://appid-1)... -Deploying application Multi-origin Active-Active Demo (US-West) (app://appid-2)... -Deploying IdP Bogus IdP to test EME-365 (idp://idpid-1)... -Deployment(s) in progress, it typically take 3 to 5 minutes -Use 'akamai eaa cert crt://certificate-UUID status' to monitor the progress. -``` - -Check the deployment status: - -```bash -$ akamai eaa cert crt://certificate-UUID status -#App/IdP ID,name,status -app://appid-1,Multi-origin Active-Active Demo (US-East),Pending -app://appid-2,Multi-origin Active-Active Demo (US-West),Pending -idp://idpid-1,Bogus IdP to test EME-365,Pending -``` +See [`akamai eaa certificate` documentation page](docs/commands/akamai-eaa-certificate.md). ### Device Posture Inventory -Pipe the result of the inventory into `jq` to display only device ID, name and user_id: - -```bash -$ akamai eaa dp inventory | jq '.[] | {device_id, device_name, user_id}' -``` +See [`akamai eaa dp` documentation page](docs/commands/akamai-eaa-dp.md). ## Known Limitations diff --git a/bin/akamai-eaa b/bin/akamai-eaa index 73d16c3..b5f7010 100755 --- a/bin/akamai-eaa +++ b/bin/akamai-eaa @@ -160,11 +160,48 @@ class ReportingAPI(BaseAPI): )) cli.footer("%s unique EAA Clients checked-in in the last 30 days" % count) + + @staticmethod + def facility_from_popname(pop_name): + """ + Crudly derive the facility based on the POP name. + """ + facility = "AWS" + if "-LIN-" in pop_name: + facility = "Akamai Cloud Compute (formerly Linode)" + return facility + def tenant_info(self): + """ + Display tenant info/stats. + """ info = {"cloudzones": []} resp = self.get('mgmt-pop/pops?shared=true') + + if self._config.show_usage: + app_apiv3 = ApplicationAPI(self._config, BaseAPI.API_Version.OpenAPIv3) + app_api = ApplicationAPI(self._config, BaseAPI.API_Version.OpenAPI) + idp_api = IdentityProviderAPI(self._config) + app_by_cz = app_apiv3.stats_by_cloudzone() + entdns_by_cz = app_api.entdns_stats_by_cloudzone() + idp_by_pop = idp_api.stats_by_pop() + + scanned_cz = [] for eaa_cloudzone in resp.json().get('objects'): - info['cloudzones'].append(eaa_cloudzone.get('region')) + cz_info = { + "name": eaa_cloudzone.get('region'), + "facility": ReportingAPI.facility_from_popname(eaa_cloudzone.get('name')), + } + if self._config.show_usage: + cz_info["count_idp"] = idp_by_pop.get(eaa_cloudzone.get('uuid_url'), 0) + cz_info["count_app"] = app_by_cz.get(eaa_cloudzone.get('region'), 0) + cz_info["count_entdns"] = entdns_by_cz.get(eaa_cloudzone.get('region'), 0) + + scanned_cz.append(cz_info) + + # sort by Cloud Zone name + info['cloudzones'] = sorted(scanned_cz, key=lambda x: x['name']) + print(dumps(info, indent=4)) def deviceposture_inventory(self, follow=False, interval=300): @@ -272,7 +309,7 @@ if __name__ == "__main__": elif config.command in ("app", "a"): a = ApplicationAPI(config) a.process_command() - elif config.command in ("connector", "c"): + elif config.command in ("connector", "c", "con"): c = ConnectorAPI(config) perf = hasattr(config, 'perf') and config.perf if config.action == "swap": @@ -280,6 +317,8 @@ if __name__ == "__main__": elif config.action == "apps": con_moniker = EAAItem(config.connector_id) c.list_apps(con_moniker, perf=perf) + elif config.action == "allowlist": + c.allow_list() else: # if command is "akamai eaa connector" we default to "list" # Unless the long form "akamai eaa connector list" is used diff --git a/bin/config.py b/bin/config.py index 17ae01c..91f35c3 100644 --- a/bin/config.py +++ b/bin/config.py @@ -93,6 +93,7 @@ def __init__(self, config_values, configuration, flags=None): syncgrp_parser.add_argument("--retry", "-r", type=int, default=0, help=argparse.SUPPRESS) # New: akamai eaa app xyz add_dnsexception www.abcd.efg # akamai eaa app xyz del_dnsexception www.abcd.efg + app_parser = subparsers.add_parser('app', aliases=["a"], help='Manage EAA applications') app_parser.add_argument(dest='application_id', help="Application ID, AppGroup ID or '-'") subsub = app_parser.add_subparsers(dest="action", help='Application action') @@ -161,11 +162,25 @@ def __init__(self, config_values, configuration, flags=None): # subparsers.required = False swap_parser = subsub.add_parser("swap", help="Swap connector with another one") swap_parser.add_argument(dest="new_connector_id", help='New connector ID') - swap_parser.add_argument('--dryrun', dest="dryrun", action="store_true", default=False, help='Dry run mode') + swap_parser.add_argument('--dryrun', dest="dryrun", action="store_true", default=False, + help='Dry run mode') + allowlist_parser = subsub.add_parser("allowlist", + help="Dump EAA Cloud Endpoint for Firewall/Proxy/Network Security equipement") + allowlist_parser.add_argument('--skip-header', dest="skip_header", action="store_true", default=False, + help='Do not print CSV header (first row)') + allowlist_parser.add_argument('--used', dest="only_used", action="store_true", default=False, + help='Show only endpoints being used by your EAA configurations') + allowlist_parser.add_argument('--fqdn', dest="fqdn", action="store_true", default=False, + help='Show Hostname instead of IP/CIDR') + allowlist_parser.add_argument('--since-time', dest="since_time", default=None, + help='Only print endpoints updated after a specific date (RFC3339)') + subparsers.add_parser('idp', aliases=["i"], help='Manage EAA Identity Providers') - subparsers.add_parser('info', help='Display tenant info') + info_parser = subparsers.add_parser('info', help='Display tenant info (cloud zone)') + info_parser.add_argument("--show-usage", dest="show_usage", action="store_true", default=False, + help="Show configuration count using each cloud zone") dp_parser = subparsers.add_parser('dp', help='Device Posture') dp_parser.add_argument("dpcommand", choices=['inventory'], default="inventory") diff --git a/docs/commands/akamai-eaa-app.md b/docs/commands/akamai-eaa-app.md new file mode 100644 index 0000000..80adce7 --- /dev/null +++ b/docs/commands/akamai-eaa-app.md @@ -0,0 +1,260 @@ +[< cli-eaa documentation](../../README.md) + +# akamai eaa app + +Manage EAA application configurations + +Alias: `a` + +## Table of contents + +- [Find and save an application](#find-and-save-an-application) +- [Restore the application](#restore-the-application) +- [Delete an application](#delete-an-application) +- [Create an new application configuration](#create-an-new-application-configuration) + - [Basic](#basic) + - [Variables and functions in the JSON configuration file](#variables-and-functions-in-the-json-configuration-file) + - [Configuration templating](#configuration-templating) +- [Functions \& variables](#functions--variables) + - [Functions](#functions) + - [include(path\_to\_file)](#includepath_to_file) + - [cli\_certificate(common\_name)](#cli_certificatecommon_name) + - [cli\_cloudzone(cloudzone\_name)](#cli_cloudzonecloudzone_name) + - [Enumeration variables](#enumeration-variables) + - [AppProfile](#appprofile) + - [AppType](#apptype) + - [AppDomainType](#appdomaintype) + + +## Find and save an application + +``` +$ akamai eaa search datascience +app://mD_Pw1XASpyVJc2JwgICTg,Data Science,akdemo-datascience,akdemo-datascience.go.akamai-access.com,4 +Found 1 app(s), total 124 app(s) +``` +Save an application: +You can save the application locally: +``` +$ akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg > ~/eaa_app_datascience_v3.json +``` + +Quickly walk through the JSON tree using `jq`. + +```bash +$ akamai eaa -b app app://mD_Pw1XASpyVJc2JwgICTg | jq .advanced_settings.websocket_enabled +"true" +``` + + +## Restore the application + +Use the `update` keyword. + +``` +$ cat ~/eaa_app_datascience_v3.json | akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg update +``` + +## Delete an application + +``` +$ akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg delete +``` + +Deploy an application, you can optionally add a comment to keep track of the change: +``` +$ akamai eaa app app://mD_Pw1XASpyVJc2JwgICTg deploy --comment "[TICKET1234] Update service account credentials" +``` + +Finding an application using a specific connector name: +*What are the applications using connector `xyz`?*\ +Use `jq` and `grep`.\ +Note: we use `-b` to avoid the extra info the CLI spills out, like the footer. + +``` +$ akamai eaa -b search | akamai eaa app - | jq -j '.name, ": ", (.agents[]|.name, " "), "\n"'|grep xyz +``` + +View groups associated with a particular application: +``` +$ akamai eaa app app://FWbUCfpvRKaSOX1rl0u55Q viewgroups +``` + +You can pipe the command as well, for example to deploy all the application matching your search (eg. "bastion") + +``` +$ akamai eaa -b search bastion | akamai eaa app - deploy +``` + +Attach/detach connectors to a particular application: + +``` +$ akamai eaa app app://app-uuid-1 attach con://connector-uuid-1 con://connector-uuid-2 +$ akamai eaa app app://app-uuid-1 detach con://connector-uuid-1 con://connector-uuid-2 +``` + +## Create an new application configuration + +### Basic + +You need to pass the application configuration JSON to the CLI using a pipe. + +``` +$ cat your-new-app.json | akamai eaa app - create +``` + +### Variables and functions in the JSON configuration file + +You can use variables and functions inside the JSON file thanks +to the Jinja templating engine. Variables are surrouded by double +curly braces, and functions/assignements open with `{%` and close +with `%}`. + +Read more about [Jinja][(https://jinja.palletsprojects.com/en/3.1.x/](https://jinja.palletsprojects.com/en/latest/)). + +Variables can be set within the JSON file, or externally using +`--var ` argument after the `create`. + +Example `cat webapp.json.j2 | akamai eaa app create --var MYVARIABLE VALUE` will allow to use DUMMY inside the code: `{{ MYVARIABLE }}`. + +
+Example of an EAA configuration with variables and functions: + +```jinja +{ + "eaa_cli_comment": [ + "This is an example of JSON with variables/functions", + "to create an application with CLI-EAA", + "To create the app, use the following command:", + "cat this_file.json.j2 | akamai eaa -v app - create" + ], + + {# Pure Jinja variable #} + {% set random_appsuffix = range(1, 10000) | random %} + + "app_profile": {{ AppProfile.HTTP.value }}, + "domain" : {{ AppDomainType.Custom.value }}, + "name": "EAA CLI Example Application Cust Domain {{ random_appsuffix }}", + "description" : "Test app to be deleted", + + "advanced_settings": { + "internal_hostname": "www.example.com", + "internal_host_port": "0" + }, + + "cert": "{{ cli_certificate('*.akamaidemo.net') }}", + "host" : "dummyspopenapidelete{{ random_appsuffix }}.akamaidemo.net", + "pop": "{{ cli_cloudzone('US-East') }}", + "servers": [ + {"origin_host": "www1.example.com", "orig_tls": "true", "origin_port": 80, "origin_protocol": "http"}, + {"origin_host": "www2.example.com", "orig_tls": "true", "origin_port": 81, "origin_protocol": "http"} + ], + "agents": [ + {"name": "demo-v2-con-1-amer", "uuid_url": "abc"}, + {"name": "demo-v2-con-2-amer", "uuid_url": "def"} + ], + "idp": { + "idp_id": "ghi" + }, + "directories": [ + { + "name": "AD Domain", + "uuid_url": "jkl" + } + ], + "groups": [ + { + "name": "Administrators", + "enable_mfa": "inherit", + "uuid_url": "196YoFHhQa-XXXXXXX" + }, + { + "name": "Support", + "enable_mfa": "inherit", + "uuid_url": "9hDCxROqTYmhXXXXXXX" + } + ] +} +``` +
+ +More example available in the [docs/examples](../examples/) directory. + +### Configuration templating + +Many parts of EAA configuration can be repeated accross multiple applications. +To save you time on generating the JSON files, you can use the Jinja2 templating. +Jinja is a templating engine adopted by tools like Ansible. + +We recommend to start by saving locally a configuration you created with Akamai +Control Center/Enterprise Center, and start splitting it into smaller file you include. + +Templating is very powerful and you might run into syntax error. We recommend to use +the `-d` option right after the `akamai eaa` command to troubleshoot. + +## Functions & variables + +### Functions + +#### include(path_to_file) + +This is a Jinja Built-in function. + +Anywhere in the JSON file, you can use the `include` function so the CLI +will find the included file and merge it into the main configuration before +passing it on to the EAA API. + +The path will be relative to where the cli command is being executed. + +Example: +```jinja +"idp": {% include 'includes/my-idp.json' %} +``` + +Where `includes/my-idp.json` will contain: +```json +{ + "client_cert_auth": "false", + "client_cert_user_param": "", + "dp_enabled": "true", + "idp_id": "abcde", + "name": "My IdP", + "type": 2 +} +``` + +#### cli_certificate(common_name) + +Lookup the custom certification with corresponding common name (CN) +inside the EAA Custom Certificate Store. +This will set the attribute/key `cert` of the application configuration. + +#### cli_cloudzone(cloudzone_name) + +Will return the cloudzone UUID. +This will set the attribute/key `pop` of the application configuration. + +### Enumeration variables + +#### AppProfile + +This is the string that indicate what type of application configuration (Web app, Tunnel App...). +The enum will set the attribute/key `app_profile` of the application configuration. + +You must use the variable with `.value` to translate from human readable to an integer EAA API can understand: + +```jinja +"app_profile": {{ AppProfile.HTTP.value }} +``` + +#### AppType + +```jinja +"app_type": {{ AppType.Tunnel.value }} +``` + +#### AppDomainType + +```jinja +"app_type": {{ AppDomainType.Akamai.value }} +``` diff --git a/docs/commands/akamai-eaa-certificate.md b/docs/commands/akamai-eaa-certificate.md new file mode 100644 index 0000000..178cdc2 --- /dev/null +++ b/docs/commands/akamai-eaa-certificate.md @@ -0,0 +1,47 @@ +[< cli-eaa documentation](../../README.md) + +# akamai eaa certificate + +Alias: `cert` + +## Display certificates + +The command `cert` displays all the certificate you have configured in EAA, along with the CN and SAN attributes in the `hosts` field, as a `+` separated list. + +Example with a wildcard certificate: + +``` +$ akamai eaa cert | head -n1 +#Certificate-ID,cn,type,expiration,days left,hosts +crt://KXi553saQSCeNI1_WH6xuA,*.akamaidemo.net,Custom,2031-06-05T22:56:34,3307,*.akamaidemo.net+akamaidemo.net +``` + +## Rotate certificates + +The cli-eaa helps with this task with the `akamai eaa certificate` command. + +Pass the certificate and key file as parameter with the optional passphrase to replace the existing certificate. +By default, the rotation does NOT redeploy the impacted application or IdP. +To trigger the re-deployment of all impacted applications and IdP, add the ``--deployafter`` flag. + +``` +$ akamai eaa certificate crt://certificate-UUID rotate --key ~/certs/mycert.key --cert ~/certs/mycert.cert --deployafter +Rotating certificate certificate-UUID... +Certificate CN: *.akamaidemo.net (*.akamaidemo.net Lets Encrypt) +Certificate certificate-UUID updated, 3 application/IdP(s) have been marked ready for deployment. +Deploying application Multi-origin Active-Active Demo (US-East) (app://appid-1)... +Deploying application Multi-origin Active-Active Demo (US-West) (app://appid-2)... +Deploying IdP Bogus IdP to test EME-365 (idp://idpid-1)... +Deployment(s) in progress, it typically take 3 to 5 minutes +Use 'akamai eaa cert crt://certificate-UUID status' to monitor the progress. +``` + +Check the deployment status: + +```bash +$ akamai eaa cert crt://certificate-UUID status +#App/IdP ID,name,status +app://appid-1,Multi-origin Active-Active Demo (US-East),Pending +app://appid-2,Multi-origin Active-Active Demo (US-West),Pending +idp://idpid-1,Bogus IdP to test EME-365,Pending +``` \ No newline at end of file diff --git a/docs/commands/akamai-eaa-connector.md b/docs/commands/akamai-eaa-connector.md new file mode 100644 index 0000000..cec7d2d --- /dev/null +++ b/docs/commands/akamai-eaa-connector.md @@ -0,0 +1,110 @@ +[< cli-eaa documentation](../../README.md) + +# akamai eaa connector + +Aliases: `c`, `con` + +Display list of connectors and their status. + +## Table of contents + +- [Usage](#usage) +- [List all EAA connectors](#list-all-eaa-connectors) +- [Swapping connectors](#swapping-connectors) +- [Show connector outbound allowlist IP/CIDR or hostnames](#show-connector-outbound-allowlist-ipcidr-or-hostnames) + +## Usage + +``` +% akamai eaa connector -h +usage: akamai eaa connector [-h] [connector_id] {apps,list,swap,allowlist} ... + +positional arguments: + connector_id Connector ID (e.g. con://abcdefghi) + {apps,list,swap,allowlist} Connector operation: + apps List applications used by the connector + list List all connectors + swap Swap connector with another one + allowlist Dump EAA Cloud Endpoint for Firewall/ + Proxy/Network Security equipement +``` + +Tips: +- pipe CLI output `column` tool available in most POSIX environment. +- when piping, the extra information is written to *stderr* so they appear seperately. + +## List all EAA connectors + +This example shows a short command `akamai eaa c`, replacing `akamai eaa connector list`: + +``` +$ akamai eaa c | column -t -s, +Total 9 connector(s) +#Connector-id name reachable status version privateip publicip debug +con://cht3_GEjQWyMW9LEk7KQfg demo-v2-con-1-amer 1 1 4.4.0-2765 10.1.4.206 12.123.123.123 Y +con://Wy0Y6FrwQ66yQzLBAInC4w demo-v2-con-2-amer 1 1 4.4.0-2765 10.1.4.172 12.123.123.123 Y +con://dK0f1UvhR7i8-RByABDXaQ demo-v2-con-4-emea 1 1 4.4.0-2765 192.168.1.90 12.123.12.12 N +con://Ihmf51dASo-R1P37hzaP3Q demo-v2-con-3-emea 1 1 4.4.0-2765 192.168.1.235 12.123.12.12 N +con://XiCmu80xQcSWnaeQcvH8Vg demo-v2-con-5-apj 1 1 4.4.0-2765 192.168.1.228 12.123.123.12 Y +con://pkGjL5OgSjyHoymMguvp9Q demo-v2-con-6-apj 1 1 4.4.0-2765 192.168.1.144 12.123.123.12 Y +con://NAWSlptPSXOjq-bk2-EQPw demo-v2-con-10-rus 1 1 4.4.0-2765 10.3.0.101 12.123.123.12 Y +con://e_0nShZBQ7esNAC3ZEkhSQ demo-v2-con-3-amer 1 1 4.4.0-2765 10.1.4.83 12.123.123.123 Y +con://OEe9o-n2S_aMeZpLxgwG0A tmelab-sfo 1 1 4.4.0-2765 192.168.2.101 12.123.123.12 Y +``` + +To integrate connector health into your monitoring system, use the `--perf` option. +`akamai eaa c list --perf` +This provides 7 extra columns: +- CPU usage (%) +- Memory usage (%) +- Network Traffic (Mbps) +- Total of dialout connections +- Idle dialout connections +- Active dialout connections + +To correlate with applications served by each connector, use the `--showapps` argument to include a list of the application FQDNs as an array in the JSON response. + +## Swapping connectors + +If you are doing a maintenance on an hypervizor, you may need to swap out 2 connectors. +The current implement look for all the apps, add the new connector, remove the old one. +The application is marked as ready to update. + +Caveats (let us know if you need it): +- This doesn't perform swap for directory +- There is no option to automatically redeploy the impacted application after the swap + +Example: +``` +$ akamai eaa connector con://e_0nShZBQ7esNAC3ZEkhSQ swap con://cht3_GEjQWyMW9LEk7KQfg +#Operation,connector-id,connector-name,app-id,app-name ++,con://cht3_GEjQWyMW9LEk7KQfg,demo-v2-con-1-amer,app://nSFDNGYARHeZGNlweIX7Wg,Speedtest (v2.1) +-,con://e_0nShZBQ7esNAC3ZEkhSQ,demo-v2-con-3-amer,app://nSFDNGYARHeZGNlweIX7Wg,Speedtest (v2.1) +Connector swapped in 1 application(s). +Updated application(s) is/are marked as ready to deploy +``` + +## Show connector outbound allowlist IP/CIDR or hostnames + +By default the command will generate a CSV on stdout with the following fields: +- Service name +- Location +- Protocol/Port +- IP/CIDR +- Last time the item was added/updated (RFC 3339) +- Number of apps consuming the Data Path + Location, requires `--used` + +The first row is the CSV header + +If `--fqdn` is used, will display only the hostname (some may come with wildcard) for Layer-7 capable security equipement such as HTTPS/TLS web proxies. + +List of Akamai Cloud Service Endpoints by IP/CIDR +``` +% akamai eaa connector allowlist +``` + +List of Akamai Cloud Service Endpoints by hostnames + +``` +% akamai eaa connector allowlist --fqdn +``` \ No newline at end of file diff --git a/docs/commands/akamai-eaa-dir.md b/docs/commands/akamai-eaa-dir.md new file mode 100644 index 0000000..157d760 --- /dev/null +++ b/docs/commands/akamai-eaa-dir.md @@ -0,0 +1,35 @@ +[< cli-eaa documentation](../../README.md) + +# akamai eaa directory + +Manage Akamai EAA directory configurations. + +Alias: `dir` + +## List the configured directories + +``` +$ akamai eaa dir +dir://FuiibQiDQzmC34oBx7INfQ,Cloud Directory,7 +dir://2Kz2YqmgSpqT_IJq9BLkWg,ad.akamaidemo.net,108 +dir://EX5-YjMyTrKgeWKHrqhUEA,Okta LDAP,10 +dir://Ygl1BpAFREiHrA8HR7dFhA,Azure AD,1 +``` + +To view the output in JSON format and in follow mode to consume Directory Health: + +``` +$ akamai eaa dir list --json --tail | jq .name +"Cloud Directory" +"AD Domain AkamaiDemo.net (global)" +"Azure AD (Sync with SCIM)" +"AKDEMO AD with UPN" +``` + +## Trigger directory synchronization + +``` +$ akamai eaa dir dir://2Kz2YqmgSpqT_IJq9BLkWg sync +Synchronize directory 2Kz2YqmgSpqT_IJq9BLkWg +Directory 2Kz2YqmgSpqT_IJq9BLkWg synchronization requested. +``` \ No newline at end of file diff --git a/docs/commands/akamai-eaa-dp.md b/docs/commands/akamai-eaa-dp.md new file mode 100644 index 0000000..1798bdc --- /dev/null +++ b/docs/commands/akamai-eaa-dp.md @@ -0,0 +1,25 @@ +[< cli-eaa documentation](../../README.md) + +# akamai eaa dp + +Manage device posture inventory. + +You can export this data feed into your own SIEM with (Akamai Unified Log Streamer)[https://github.com/akamai/uls]. + +## View device inventory + +Pipe the result of the inventory into `jq` to display only device ID, name and user_id: + +```bash +$ akamai eaa dp inventory | jq '.[] | {device_id, device_name, user_id}' +``` + +## Watch (tail) the inventory + +By default the cli will poll and print data every 10 minutes. +You can increase the interval by using the `--interval ` argument. + +```bash +$ akamai eaa dp inventory --tail +``` + diff --git a/docs/commands/akamai-eaa-log.md b/docs/commands/akamai-eaa-log.md new file mode 100644 index 0000000..2859194 --- /dev/null +++ b/docs/commands/akamai-eaa-log.md @@ -0,0 +1,65 @@ +[< cli-eaa documentation](../../README.md) + +# akamai eaa log + +Access to EAA security events and logs. + +Alias: `l` + +## Table of contents + +- [Command description](#command-description) +- [View access log in tail mode](#view-access-log-in-tail-mode) +- [View access log with specific date/time boundaries](#view-access-log-with-specific-datetime-boundaries) +- [Send access logs to a file](#send-access-logs-to-a-file) +- [Audit logs](#audit-logs) + +## Command description + +EAA comes with two types of logs, the user access logs and the administrator audit logs. +For detailed description for each event field, refer to the product documentation on [https://techdocs.akamai.com](https://techdocs.akamai.com/eaa/docs/data-feed-siem). + +You can retrieve EAA events one of these ways: +- in near realtime using the argument `-f` or `--tail`. If you set `-f` and date range, the `-f` option will be ignored. +- retrieve a period of time based on EPOCH timestamp in `--start` and `--end` +- tune the acceptable delay vs. completeness with `--delay`. We recommend 10 minutes delay for full completeness. + +You can also export this data feed into your own SIEM with (Akamai Unified Log Streamer)[https://github.com/akamai/uls]. + +## View access log in tail mode + +Pull user access logs, block until new logs are received. +You can stop using Control+C (Control+Break) or sending a SIG_INT or SIG_TERM signal to the process. + +```bash +$ akamai eaa log access --tail +``` + +## View access log with specific date/time boundaries + +You may want a one time chunk of log for a period of time, for example, the last 6 hours: + +```bash +$ START=$(bc <<< "$(date +%s) - 6 * 60 * 60") +$ akamai eaa log access -s $START +``` + +On Windows platforms, you can use PowerShell: +```powershell +PS /home/cli-eaa> $START = (Get-Date -UFormat %s) - 6 * 60 * 60 +PS /home/cli-eaa> akamai eaa log access -s $START +``` + +## Send access logs to a file + +Send the **user access events** to a file (utf-8 encoding is being used): +```bash +$ akamai eaa log access --tail -o /tmp/eaa_access.log +``` + +## Audit logs + +Pull **admin audit events**, block till new logs are received +```bash +$ akamai eaa log admin --tail +``` diff --git a/docs/examples/includes/akdemo-amer-2maincon.json b/docs/examples/includes/akdemo-amer-2maincon.json new file mode 100644 index 0000000..78c4229 --- /dev/null +++ b/docs/examples/includes/akdemo-amer-2maincon.json @@ -0,0 +1,12 @@ +[ + { + "compatible": true, + "name": "connector-1", + "uuid_url": "abc" + }, + { + "compatible": true, + "name": "connector-2", + "uuid_url": "def" + } +] \ No newline at end of file diff --git a/docs/examples/includes/akdemo-directory.json b/docs/examples/includes/akdemo-directory.json new file mode 100644 index 0000000..d00ca83 --- /dev/null +++ b/docs/examples/includes/akdemo-directory.json @@ -0,0 +1,8 @@ +[ + { + "name": "AD Domain", + "type": 1, + "user_count": 123, + "uuid_url": "defgth" + } +] \ No newline at end of file diff --git a/docs/examples/includes/akdemo-groups-default.json b/docs/examples/includes/akdemo-groups-default.json new file mode 100644 index 0000000..b91d38d --- /dev/null +++ b/docs/examples/includes/akdemo-groups-default.json @@ -0,0 +1,27 @@ +[ + { + "name": "Sales Department", + "enable_mfa": "inherit", + "uuid_url": "16nUm7dCSyiihfCvMiXXXX" + }, + { + "name": "IT Department", + "enable_mfa": "inherit", + "uuid_url": "yvjnNqoDQt2P0S1Q2eXXXX" + }, + { + "name": "Marketing Department", + "enable_mfa": "inherit", + "uuid_url": "CxhdrFKnQG2mBSUBV9XXXX" + }, + { + "name": "Engineering Department", + "enable_mfa": "inherit", + "uuid_url": "zueKU3XmTsm9jGw7RsXXXX" + }, + { + "name": "Domain Users", + "enable_mfa": "inherit", + "uuid_url": "iOlcvuOgSmCYaYDgvpXXXX" + } +] \ No newline at end of file diff --git a/docs/examples/includes/akdemo-idp.json b/docs/examples/includes/akdemo-idp.json new file mode 100644 index 0000000..b5b9795 --- /dev/null +++ b/docs/examples/includes/akdemo-idp.json @@ -0,0 +1,8 @@ +{ + "client_cert_auth": "false", + "client_cert_user_param": "", + "dp_enabled": "true", + "idp_id": "abc", + "name": "Global IdP", + "type": 2 +} \ No newline at end of file diff --git a/libeaa/application.py b/libeaa/application.py index 03b211d..65b36b6 100644 --- a/libeaa/application.py +++ b/libeaa/application.py @@ -74,8 +74,9 @@ class ClientMode(Enum): TCP = 1 Tunnel = 2 - def __init__(self, config): - super(ApplicationAPI, self).__init__(config, api=BaseAPI.API_Version.OpenAPI) + def __init__(self, config, api=BaseAPI.API_Version.OpenAPI): + "Force the config arg to passed on." + super(ApplicationAPI, self).__init__(config, api=api) def process_command(self): """ @@ -168,34 +169,36 @@ def load(self, app_moniker, expand=True): # hence the two expand parameters url_params = {} if expand: - url_params = {'expand': 'true', 'expand_sdk': 'true'} + url_params = {'expand': expand, 'expand_sdk': expand} url = 'mgmt-pop/apps/{applicationId}'.format(applicationId=app_moniker.uuid) result = self.get(url, params=url_params) app_config = result.json() - # Merge application groups info and align the view - # to match the structure expected in save operation - groups_url = 'mgmt-pop/apps/{applicationId}/groups'.format(applicationId=app_moniker.uuid) - groups_result = self.get(groups_url, params={'limit': 0}) - groups = groups_result.json() - app_config['groups'] = [] - for g in groups.get('objects', []): - app_config['groups'].append( - { - 'name': g.get('group', {}).get('name'), - 'enable_mfa': g.get('enable_mfa', 'inherit'), - 'uuid_url': g.get('group', {}).get('group_uuid_url') - } - ) + if expand: + + # Merge application groups info and align the view + # to match the structure expected in save operation + groups_url = 'mgmt-pop/apps/{applicationId}/groups'.format(applicationId=app_moniker.uuid) + groups_result = self.get(groups_url, params={'limit': 0}) + groups = groups_result.json() + app_config['groups'] = [] + for g in groups.get('objects', []): + app_config['groups'].append( + { + 'name': g.get('group', {}).get('name'), + 'enable_mfa': g.get('enable_mfa', 'inherit'), + 'uuid_url': g.get('group', {}).get('group_uuid_url') + } + ) - # Merge URL path-based policies and align the view - # to match the structure expected in save operation - upp_url = 'mgmt-pop/apps/{applicationId}/urllocation' - upp_result = self.get(upp_url.format(applicationId=app_moniker.uuid), params={'limit': 0}) - upp = upp_result.json() - app_config['urllocation'] = [] - for upp_rule in upp.get('objects', []): - app_config['urllocation'].append(upp_rule) + # Merge URL path-based policies and align the view + # to match the structure expected in save operation + upp_url = 'mgmt-pop/apps/{applicationId}/urllocation' + upp_result = self.get(upp_url.format(applicationId=app_moniker.uuid), params={'limit': 0}) + upp = upp_result.json() + app_config['urllocation'] = [] + for upp_rule in upp.get('objects', []): + app_config['urllocation'].append(upp_rule) return app_config @@ -587,3 +590,56 @@ def deploy(self, app_moniker, comment=""): logger.info("ApplicationAPI: deploy app response: %s" % deploy.status_code) if deploy.status_code != 200: logger.error(deploy.text) + + def list(self, details=False): + """ + List all applications (experimental) + :param details (bool): load the app details (SUPER SLOW) + """ + + if self.api_ver != BaseAPI.API_Version.OpenAPIv3: + raise Exception("Unsupported API version") + + total_count = None + page = 1 + page_size = 25 + offset = 0 + l = [] + + app_config_loader = ApplicationAPI(self._config, BaseAPI.API_Version.OpenAPI) + while total_count == None or len(l) < total_count: + r = self.get('mgmt-pop/apps', params={"offset": offset, "fields": "uuid_url", + "limit": page_size, "offset": (page-1)*page_size}) + j = r.json() + total_count = j.get('meta').get('total_count') + page += 1 + for a in j.get('objects'): + if details: + l.append(app_config_loader.load(EAAItem(f"app://{a.get('uuid_url')}"), expand=False)) + else: + l.append(a) + return l + + def stats_by_cloudzone(self): + if self.api_ver != BaseAPI.API_Version.OpenAPIv3: + raise Exception("Unsupported API version") + + appcount_by_cloudzone = {} + for a in self.list(True): + scanned_cloudzone = a.get('popRegion') + if scanned_cloudzone not in appcount_by_cloudzone.keys(): + appcount_by_cloudzone[scanned_cloudzone] = 0 + appcount_by_cloudzone[scanned_cloudzone] += 1 + return appcount_by_cloudzone + + def entdns_stats_by_cloudzone(self): + "Special app 'Enterprise DNS'." + entdns_count_by_cloudzone = {} + r = self.get("mgmt-pop/childdns?limit=0") + entdns_response = r.json() + for e in entdns_response.get('objects'): + scanned_cloudzone = e.get('popRegion') + if scanned_cloudzone not in entdns_count_by_cloudzone.keys(): + entdns_count_by_cloudzone[scanned_cloudzone] = 0 + entdns_count_by_cloudzone[scanned_cloudzone] += 1 + return entdns_count_by_cloudzone diff --git a/libeaa/common.py b/libeaa/common.py index 02574e0..13a79b9 100644 --- a/libeaa/common.py +++ b/libeaa/common.py @@ -26,7 +26,7 @@ import hmac import hashlib from urllib.parse import urljoin, parse_qs -from enum import Enum +from enum import Enum, IntEnum # 3rd party libs import six @@ -185,10 +185,11 @@ def __neq__(self, other): class BaseAPI(object): - class API_Version(Enum): + class API_Version(IntEnum): "API backend, either Legacy or {OPEN} API (support introduced in 2020 for EAA)." Legacy = 1 - OpenAPI = 2 + OpenAPI = 2, + OpenAPIv3 = 3 def __init__(self, config=None, api=API_Version.Legacy): @@ -216,7 +217,10 @@ def __init__(self, config=None, api=API_Version.Legacy): ) self._session.mount("https://", siem_api_adapter) else: # EAA {OPEN} API - self._baseurl = 'https://%s/crux/v1/' % edgerc.get(section, 'host') + if self.api_ver == self.API_Version.OpenAPI: + self._baseurl = f'https://%s/crux/v1/' % edgerc.get(section, 'host') + elif self.api_ver == self.API_Version.OpenAPIv3: + self._baseurl = f'https://%s/crux/v3/' % edgerc.get(section, 'host') self._session = requests.Session() self._session.auth = EdgeGridAuth.from_edgerc(edgerc, section) # Handle extra querystring to send to all REST requests diff --git a/libeaa/connector.py b/libeaa/connector.py index 4d848b0..57ca81b 100644 --- a/libeaa/connector.py +++ b/libeaa/connector.py @@ -19,6 +19,10 @@ import signal import datetime from functools import lru_cache +import io +import csv +from dateutil.parser import parse +import pytz from common import cli, BaseAPI, EAAItem @@ -242,6 +246,48 @@ def all_apps(self, exp): search_app = self.get('mgmt-pop/apps', params=url_params) return search_app.json() + def allow_list(self): + """ + Print the Connector Allow List of endpoint (IP/CIDR or host) + as a CSV output. + """ + appcount_by_cloudzone = {} + later_than = None + if self._config.since_time: + later_than = parse(self._config.since_time) + if not later_than.tzname(): + later_than = pytz.utc.localize(later_than) + + if self._config.only_used: + app_factory = ApplicationAPI(self._config, BaseAPI.API_Version.OpenAPIv3) + appcount_by_cloudzone = app_factory.stats_by_cloudzone() + + csv_output = io.StringIO() + ep_fmt = "IP/CIDR" + if self._config.fqdn: + ep_fmt = "FQDN" + + csv_writer = csv.writer(csv_output, quoting=csv.QUOTE_MINIMAL) + if not self._config.skip_header: + fieldnames = ['Service', 'Location', 'Protocol', ep_fmt, 'LastUpdate', 'Apps'] + csv_writer.writerow(fieldnames) + r = self.get("zt/outboundallowlist") + for l in r.json(): + d = datetime.datetime.fromisoformat(l.get('modifiedDate')) + used_apps = appcount_by_cloudzone.get(l.get('location')) + if later_than and d < later_than: + continue + if self._config.fqdn: + hosts = l.get('host').split(",") + gen = (h for h in hosts if h not in ('-', )) + for h in gen: + csv_writer.writerow([l.get('service'), l.get('location'), l.get('protocol'), h.strip(), l.get('modifiedDate'), used_apps]) + else: + for ip in l.get('ips', []): + csv_writer.writerow([l.get('service'), l.get('location'), l.get('protocol'), ip.get('ip'), ip.get('modifiedDate'), used_apps]) + + cli.print(csv_output.getvalue()) + def findappbyconnector(self, connector_moniker): """ Find EAA Applications using a particular connector. diff --git a/libeaa/idp.py b/libeaa/idp.py index 47a7081..1ec5a21 100644 --- a/libeaa/idp.py +++ b/libeaa/idp.py @@ -56,3 +56,21 @@ def deploy(self, idp_moniker): if deploy_idp.status_code != 200: raise Exception("Error deploying IdP %s HTTP %s" % (idp_moniker, deploy_idp.status_code)) + + def stats_by_pop(self): + """ + Build a dictionnary with key being the EAA POP name, + and value the number of IdP configuration deployed. + """ + idp_by_pop_uuid = {} + url_params = {'limit': MAX_RESULT} + search_idp = self.get('mgmt-pop/idp', params=url_params) + idps = search_idp.json() + + for idp in idps.get('objects'): + pop_uuid = idp.get('pop') + if not pop_uuid in idp_by_pop_uuid.keys(): + idp_by_pop_uuid[pop_uuid] = 0 + idp_by_pop_uuid[pop_uuid] += 1 + + return idp_by_pop_uuid diff --git a/requirements.txt b/requirements.txt index 8222ca5..85266ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ requests six edgegrid-python jinja2 +pytz +python-dateutil \ No newline at end of file diff --git a/test/test.py b/test/test.py index 0575116..e782ae8 100644 --- a/test/test.py +++ b/test/test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Akamai Technologies, Inc. All Rights Reserved +# Copyright 2024 Akamai Technologies, Inc. All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ See also https://pytest-html.readthedocs.io/en/latest/ .. code-block:: bash + pip install pytest-html cd test # Specify the test app URL to generate traffic against URL_TEST_TRAFFIC=https://login:password@myclassicapp.go.akamai-access.com pytest --html=report.html --self-contained-html test.py @@ -158,7 +159,7 @@ def config_testapp_url(cls): """ Generate some traffic against a webapp defined """ - delay = 30 + delay = 60 url = os.getenv('URL_TEST_TRAFFIC') if url: CliEAATest.cli_print(f"Test fingerprint: {id(cls):x}") @@ -178,6 +179,7 @@ def test_useraccess_log_raw(self): """ Fetch User Access log events (RAW format) """ + self.assertNotEqual(os.environ.get('URL_TEST_TRAFFIC'), "", "set URL_TEST_TRAFFIC env for this test") cmd = self.cli_run("log", "access", "--start", self.after, "--end", self.before) stdout, stderr = cmd.communicate(timeout=60) events = stdout.decode(encoding) @@ -189,11 +191,12 @@ def test_useraccess_log_raw_v2(self): """ Fetch User Access log events (RAW format) using the API v2 introduced in EAA 2021.02 """ - cmd = self.cli_run("log", "access", "-2", "--start", self.after, "--end", self.before) + self.assertNotEqual(os.environ.get('URL_TEST_TRAFFIC'), "", "set URL_TEST_TRAFFIC env for this test") + cmd = self.cli_run("log", "access", "--start", self.after, "--end", self.before) stdout, stderr = cmd.communicate(timeout=60) events = stdout.decode(encoding) event_count = len(events.splitlines()) - self.assertGreater(event_count, 0, "We expect at least one user access event, set URL_TEST_TRAFFIC env") + self.assertGreater(event_count, 0, "We expect at least one user access event") self.assertEqual(cmd.returncode, 0, 'return code must be 0') def test_admin_log_raw(self): @@ -231,7 +234,7 @@ def test_useraccess_log_json_v2(self): """ Fetch User Access log events (JSON format) """ - cmd = self.cli_run("log", "access", "-2", "--start", self.after, "--end", self.before, "--json") + cmd = self.cli_run("log", "access", "--start", self.after, "--end", self.before, "--json") stdout, stderr = cmd.communicate(timeout=60) scanned_events = stdout.decode(encoding) lines = scanned_events.splitlines() @@ -322,6 +325,32 @@ def test_connector_health_tail(self): CliEAATest.cli_print("stderr>", l) self.assertGreater(len(stdout), 0, "No connector health output") + def test_connector_allowlist_ipcidr(self): + cmd = self.cli_run('-d', '-v', 'c', 'allowlist') + stdout, stderr = cmd.communicate(timeout=50.0) + for l in stdout.splitlines(): + CliEAATest.cli_print("stdout>", l) + for l in stderr.splitlines(): + CliEAATest.cli_print("stderr>", l) + self.assertGreater(len(stdout), 0, "No allowlist IP/CIDR output") + + def test_connector_allowlist_fqdn(self): + cmd = self.cli_run('-d', '-v', 'c', 'allowlist', '--fqdn') + stdout, stderr = cmd.communicate(timeout=50.0) + for l in stdout.splitlines(): + CliEAATest.cli_print("stdout>", l) + for l in stderr.splitlines(): + CliEAATest.cli_print("stderr>", l) + self.assertGreater(len(stdout), 0, "No allowlist hostname output") + + def test_connector_allowlist_sincetime(self): + cmd = self.cli_run('-d', '-v', 'c', 'allowlist', '--since-time', '2000-01-01T00:00:00.0000000Z') + stdout, stderr = cmd.communicate(timeout=50.0) + for l in stdout.splitlines(): + CliEAATest.cli_print("stdout>", l) + for l in stderr.splitlines(): + CliEAATest.cli_print("stderr>", l) + self.assertGreater(len(stdout), 0, "No allowlist IP/CIDR output with change set before Jan 1, 2000") class TestIdentity(CliEAATest): @@ -394,5 +423,14 @@ def test_cli_info(self): stdout, stderr = cmd.communicate() self.assertEqual(cmd.returncode, 0, 'return code must be 0') + def test_cli_info_usage(self): + """ + Display tenant info with usage details + """ + cmd = self.cli_run('info', '--show-usage') + stdout, stderr = cmd.communicate(timeout=120) + self.assertEqual(cmd.returncode, 0, 'return code must be 0') + + if __name__ == '__main__': unittest.main()