diff --git a/docs/class4/.DS_Store b/docs/class4/.DS_Store index a643a7f..24b9b9d 100644 Binary files a/docs/class4/.DS_Store and b/docs/class4/.DS_Store differ diff --git a/docs/class4/module4/.DS_Store b/docs/class4/module4/.DS_Store index d86cb3d..0dd36d5 100644 Binary files a/docs/class4/module4/.DS_Store and b/docs/class4/module4/.DS_Store differ diff --git a/docs/class4/module4/lab1/lab1.rst b/docs/class4/module4/lab1/lab1.rst index 13d7136..dcd0562 100644 --- a/docs/class4/module4/lab1/lab1.rst +++ b/docs/class4/module4/lab1/lab1.rst @@ -17,9 +17,44 @@ The LAD Server acts as a syslog receiver collecting access/request logs from the :scale: 50% -JWT Access Control ------------------- +Run LAD installer +----------------- +* SSH or WebSSH to LAD Server machine +* Go to folder /home/ubuntu/LAD_RELEASE-v0.3.0 +* Run the installer -Enable JWT Access Control -^^^^^^^^^^^^^^^^^^^^^^^^^ + .. code-block:: bash + + ./install_local.sh + +* Keep all settings by default - just click ``enter`` key +* Wait till the installer finishes !!!! + +You will see the environment details + +.. note:: + 💡 Your environment is now accessible via the following links: + + ========= ====================== ============= ============ ======================= + Name URL User Password Description + ========= ====================== ============= ============ ======================= + Dashboard https://localhost:8000 N/A N/A API Discovery Dashboard + ========= ====================== ============= ============ ======================= + + 🌐 Usable IP addresses on this host: 10.1.1.9, 10.1.20.9, 172.17.0.1, 172.18.0.1 + + +Connect to LAD Console +---------------------- + +* In UDF, in the LAD Server machine, select ``LAD UI`` +* Connect with ``admin`` / ``admin`` +* You are asked to change the password, change with ``F5twister$2026`` +* Sign-In + +.. image:: ../pictures/lad-page.png + :align: left + :scale: 50% + +Next step is to configure the BIG-IP to send traffic datas to LAD server diff --git a/docs/class4/module4/lab2/lab2.rst b/docs/class4/module4/lab2/lab2.rst index aa04193..13f685f 100644 --- a/docs/class4/module4/lab2/lab2.rst +++ b/docs/class4/module4/lab2/lab2.rst @@ -1,13 +1,586 @@ Configure BIG-IP for Local API Discovery ======================================== -There are multiple options to do Rate Limiting in F5XC. In this lab, we are focusing on API Protection Rate Limiting. +Now, we must configure the BIG-IP to collect the datas, format the datas, and send the datas to the LAD server. -The goal is to rate limit an endpoint at risk because we discovered an attack or it is a shadow API and we are not sure if we should allow or block it. +There is a how-to into the LAD Console for TMUI, iControl REST and TMSH. -Enable Rate Limiting from the Security Dashboard ------------------------------------------------- +Click on ``Integration`` > ``Integration Guide`` +.. image:: ../pictures/integration1.png + :align: left + :scale: 50% -Test your Rate Limiting config ------------------------------- +You can see how it works and how it is interconnected. + +In a nutshell, an irule attached to the API Application Virtual Server is collecting the requests and responses, formating the datas and send those datas to a pool where the LAD is a member. + +.. image:: ../pictures/traffic-flow2.png + :align: left + :scale: 50% + +Configure the BIG-IP via the TMUI +--------------------------------- + +Login to the BIGIP as ``admin`` / ``admin`` + +Configure the External Pool +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This pool is used to send the datas to the LAD server + +* Go to Local Traffic -> Pools and Create a new External pool for LAD Collector. +* Name (use this name): logging-node-tls +* Member IP address is the LADT internal Self-IP : ``10.1.20.9`` +* Pool member port must be ``6514`` + +.. image:: ../pictures/pool_list.png + :align: left + :scale: 50% + +Configure the Internal VS +^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Configure a VS named ``syslog-tls-virtual`` +* Destination Address : can be anything as it is internal. Let's put ``2.2.2.2`` +* With service port ``6514`` +* Server SSL Profile : ``serverssl`` +* Pool : ``logging-node-tls`` (created in the previous step) + +.. image:: ../pictures/config-vs.png + :align: left + :scale: 50% + +Configure the Internal Logging Pool +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This Pool is used to target the Internal VS created previously. It will be called by the iRule we are going to create. + +* Go to Local Traffic -> Pools and Create a new Internal pool +* Name (use this name) : ``logging-pool-tls`` +* Member IP Address is the Internal VS IP. We set ``2.2.2.2``, so use this IP as a member +* Pool member port is ``6514`` + +.. image:: ../pictures/internal-pool.png + :align: left + :scale: 50% + +Create the iRule and assign to the Internal VS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Go to Local Traffic -> iRules -> iRules list +* Create the iRules with the code below. Name the iRules ``MyApiDiscoveryRule`` +* Copy the irule. There is a Copy button below, on the top right corner, to copy the full text. + +.. note:: + + Check that your Internal Pool name is ``logging-pool-tls``. This name is used into this iRule. If you have followed the above steps and used the same names as defined in the lab, the iRule is ready and can be copy/paste. + + .. image:: ../pictures/irule2.png + :align: left + +.. code-block:: bash + + #Created by F5ers Matt S. & Mike W. + when RULE_INIT { + # Debug flag - set to 0 to disable local logging + set static::debug 1 + + if {[catch { + # Set sampling rate + # Pre-compile regular expressions for better performance + set static::timestamp_format "%Y-%m-%dT%T.000Z" + set static::json_template "\"uri\":\"%s\",\"host\":\"%s\",\"method\":\"%s\",\"statusCode\":\"%s\"" + set static::sampling_rate 1 + set static::rate_limit 100 + set static::email_regex {[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}} + set static::ssn_regex {(\d{3}-\d{2}-\d{4})} + set static::cc_regex {(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35[0-9]{3})[0-9]{11})} + set static::json_ssn_regex {"ssn":\s*"(\d{3}-\d{2}-\d{4})"} + set static::json_dob_regex {"dob":\s*"(\d{4}-\d{2}-\d{2})"} + set static::json_email_regex {"email":\s*"([^"]+@[^"]+)"} + set static::dob_regex {(\d{4}-\d{2}-\d{2}|\d{2}/\d{2}/\d{4}|\d{2}-\d{2}-\d{4}|\d{2}\.\d{2}\.\d{4})} + set static::valid_years_start 1900 + set static::valid_years_end 2099 + # Luhn check optimization - moved to static context + set static::luhn_check { + set sum 0 + set alternate 0 + set card_number [string map {" " ""} $card_number] + + for {set i [string length $card_number]} {$i > 0} {incr i -1} { + scan [string index $card_number [expr {$i - 1}]] %d digit + if {$alternate} { + set digit [expr {($digit * 2 > 9) ? ($digit * 2 - 9) : ($digit * 2)}] + } + incr sum $digit + set alternate [expr {!$alternate}] + } + expr {$sum % 10 == 0} + } + } err]} { + log local0.error "Error in RULE_INIT: $err" + } + + if {$static::debug} { + log local0. "API Discovery iRule initialized with debug logging enabled" + } + } + + when CLIENT_ACCEPTED { + if {[catch { + set processRequest 0 + set z1_logging logging-pool-tls + set z1_remoteLogProtocol TCP + set hsl [HSL::open -proto $z1_remoteLogProtocol -pool $z1_logging] + } err]} { + log local0.error "Error in CLIENT_ACCEPTED: $err" + return + } + } + + when HTTP_REQUEST { + if {[catch { + set processRequest 0 + ## Check if count table exists. If not, initalize with 1 to avoid null errors. Auto-reset count after 180 seconds to keep the table small. + if {[table lookup -notouch "request_counter"] < 1} { + table set "request_counter" 1 180 180 + } else { + ## Table exists. Increment request count. + set current_count [table incr -notouch "request_counter"] + } + # Determine if this request should be processed + if {[expr {[table lookup -notouch "request_counter"] % $static::sampling_rate == 0}]} { + set processRequest 1 + if {$static::debug} { + log local0. "Processing request (counter=[table lookup -notouch "request_counter"])" + } + # Initialize variables once + array set z1_request_data { + sensitive_in_payload false + sensitive_in_headers false + sensitive_data_types {} + payload_type "Unknown" + has_auth false + xff "" + clientsslprofile "none" + req_content_type "" + } + + if {[catch { + # Store non-header HTTP request data + set z1_request_data(http_uri) [HTTP::uri] + set z1_request_data(http_host) [HTTP::host] + set z1_request_data(http_method) [HTTP::method] + set z1_request_data(http_version) [HTTP::version] + set z1_request_data(client_ip) [IP::client_addr] + set z1_request_data(client_port) [TCP::client_port] + set z1_request_data(virtual_server) [virtual name] + + # Per-host rate limiting (100 req/sec) + set z1_global_counter 0 + set rl_host $z1_request_data(http_host) + set now_sec [clock seconds] + # Track per-second request count for this host + set rate_key "z1:rate:$rl_host:$now_sec" + if {[table lookup -notouch $rate_key] eq ""} { + table set $rate_key 0 2 2 + } + set current_rate [table incr $rate_key] + + # Use a per-host-per-second counter for suppressed logs + set ex_key_current "z1:ex:$rl_host:$now_sec" + + # Decide whether to allow per-request log or accumulate + if {$current_rate <= $static::rate_limit} { + set z1_rate_allow 1 + # Elect a single "first allowed" request for this host in the current second (atomic via incr) + set first_key "z1:first:$rl_host:$now_sec" + set first_count [table incr $first_key] + # Ensure the marker expires quickly + table set $first_key $first_count 2 2 + if {$first_count == 1} { + # Drain suppressed logs from the previous second only + set prev_sec [expr {$now_sec - 1}] + set prev_key "z1:ex:$rl_host:$prev_sec" + set pending_excess [table lookup -notouch $prev_key] + if {$pending_excess ne "" && $pending_excess > 0} { + set z1_global_counter $pending_excess + table delete $prev_key + } else { + set z1_global_counter 0 + } + } else { + set z1_global_counter 0 + } + } else { + set z1_rate_allow 0 + # Accumulate suppressed count for this host in the current second + if {[table lookup -notouch $ex_key_current] eq ""} { + table set $ex_key_current 0 3 3 + } + table incr $ex_key_current + } + + # Initialize card_number for proper scope + set card_number "" + + # Process all headers in a single pass + foreach header_name [HTTP::header names] { + set header_value [HTTP::header $header_name] + + # Handle specific headers directly + switch -exact -- $header_name { + "Content-Type" { set z1_request_data(req_content_type) $header_value } + "Referer" { set z1_request_data(http_referrer) $header_value } + "User-Agent" { set z1_request_data(http_user_agent) $header_value } + "Authorization" { set z1_request_data(has_auth) true } + "X-Forwarded-For" { + set xff_ips [split $header_value ","] + set z1_request_data(xff) [string trim [lindex $xff_ips end]] + } + } + + # Process for sensitive data + # SSN Check with date format exclusion + if {[regexp $static::ssn_regex $header_value -> ssn]} { + scan $ssn {%d-%d-%d} d1 d2 d3 + if {$d1 >= 1 && $d1 <= 12} { + if {$static::debug} { + log local0. "Skipping SSN check for date-like pattern in header" + } + } else { + # Validate SSN + if {$d1 >= 1 && $d1 <= 899 && $d1 != 666 && + $d2 >= 1 && $d2 <= 99 && + $d3 >= 1 && $d3 <= 9999} { + lappend z1_request_data(sensitive_data_types) "SSN" + set z1_request_data(sensitive_in_headers) true + } + } + } + + # Credit card check with optimized Luhn + if {[regexp $static::cc_regex $header_value cc_number]} { + set card_number $cc_number + if {[eval $static::luhn_check]} { + lappend z1_request_data(sensitive_data_types) "CreditCard" + set z1_request_data(sensitive_in_headers) true + } + } + + # Email check + if {[regexp $static::email_regex $header_value]} { + lappend z1_request_data(sensitive_data_types) "Email" + set z1_request_data(sensitive_in_headers) true + } + + # DOB check with multiple format support + if {[regexp $static::dob_regex $header_value dob]} { + if {[catch { + switch -regexp $dob { + {\d{4}-\d{2}-\d{2}} { + scan $dob {%d-%d-%d} year month day + } + {\d{2}/\d{2}/\d{4}} { + scan $dob {%d/%d/%d} month day year + } + {\d{2}-\d{2}-\d{4}} { + scan $dob {%d-%d-%d} month day year + } + {\d{2}\.\d{2}\.\d{4}} { + scan $dob {%d.%d.%d} month day year + } + } + + if {$month >= 1 && $month <= 12 && + $day >= 1 && $day <= 31 && + $year >= $static::valid_years_start && + $year <= $static::valid_years_end} { + lappend z1_request_data(sensitive_data_types) "DOB" + set z1_request_data(sensitive_in_headers) true + } + } err]} { + log local0.error "Error processing DOB: $err" + } + } + } + } err]} { + log local0.error "Error collecting request data: $err" + } + + # Collect payload if present + if {[HTTP::header "Content-Length"] ne ""} { + if {[catch { + set z1_req_length [HTTP::header "Content-Length"] + if {[string is integer -strict $z1_req_length] && $z1_req_length > 0} { + HTTP::collect $z1_req_length + } + } err]} { + log local0.error "Error collecting payload: $err" + } + } + } + } err]} { + log local0.error "Error in HTTP_REQUEST: $err" + } + } + + when HTTP_REQUEST_DATA { + if { $processRequest == 0} { return } + + if {[catch { + if {[HTTP::payload length] > 0} { + set z1_payload [HTTP::payload] + + # Determine payload type efficiently + if {[catch { + if {$z1_request_data(http_version) eq "2.0" && $z1_request_data(req_content_type) eq "application/grpc"} { + set z1_request_data(payload_type) "gRPC" + } else { + set trimmed_payload [string trimleft $z1_payload] + set first_char [string index $trimmed_payload 0] + + switch -exact -- $first_char { + "\[" { set z1_request_data(payload_type) "REST_ARRAY" } + "\{" { set z1_request_data(payload_type) "REST" } + "<" { + if {[string first " ssn]} { + scan $ssn {%d-%d-%d} d1 d2 d3 + if {$d1 >= 1 && $d1 <= 899 && $d1 != 666 && + $d2 >= 1 && $d2 <= 99 && + $d3 >= 1 && $d3 <= 9999} { + lappend z1_request_data(sensitive_data_types) $type + set z1_request_data(sensitive_in_payload) true + } + } + } + "CreditCard" { + if {[regexp $pattern $z1_payload cc_number]} { + set card_number $cc_number + if {[eval $static::luhn_check]} { + lappend z1_request_data(sensitive_data_types) $type + set z1_request_data(sensitive_in_payload) true + } + } + } + "Email" { + if {[regexp $pattern $z1_payload]} { + lappend z1_request_data(sensitive_data_types) $type + set z1_request_data(sensitive_in_payload) true + } + } + "DOB" { + if {[regexp $pattern $z1_payload dob]} { + switch -regexp $dob { + {\d{4}-\d{2}-\d{2}} { + scan $dob {%d-%d-%d} year month day + } + {\d{2}/\d{2}/\d{4}} { + scan $dob {%d/%d/%d} month day year + } + {\d{2}-\d{2}-\d{4}} { + scan $dob {%d-%d-%d} month day year + } + {\d{2}\.\d{2}\.\d{4}} { + scan $dob {%d.%d.%d} month day year + } + } + + if {$month >= 1 && $month <= 12 && + $day >= 1 && $day <= 31 && + $year >= $static::valid_years_start && + $year <= $static::valid_years_end} { + lappend z1_request_data(sensitive_data_types) $type + set z1_request_data(sensitive_in_payload) true + } + } + } + } + } + } err]} { + log local0.error "Error processing patterns in payload: $err" + } + + # Type-specific processing + if {[catch { + switch -- $z1_request_data(payload_type) { + "REST" - "REST_ARRAY" { + foreach {pattern type} [list \ + $static::json_ssn_regex "SSN" \ + $static::json_dob_regex "DOB" \ + $static::json_email_regex "Email"] { + if {[regexp $pattern $z1_payload]} { + if {[lsearch $z1_request_data(sensitive_data_types) $type] == -1} { + lappend z1_request_data(sensitive_data_types) $type + set z1_request_data(sensitive_in_payload) true + } + } + } + } + + "XML" - "SOAP" { + if {[regexp {<[^>]+>([^<]+)} $z1_payload -> content]} { + foreach {pattern type} [list \ + $static::ssn_regex "SSN" \ + $static::email_regex "Email" \ + $static::dob_regex "DOB"] { + if {[regexp $pattern $content]} { + if {[lsearch $z1_request_data(sensitive_data_types) $type] == -1} { + lappend z1_request_data(sensitive_data_types) $type + set z1_request_data(sensitive_in_payload) true + } + } + } + } + } + } + } err]} { + log local0.error "Error processing payload type-specific patterns: $err" + } + } + } err]} { + log local0.error "Error in HTTP_REQUEST_DATA: $err" + } + } + + ## Exit gracefully if this request is not marked for processing. + when LB_SELECTED { + if { $processRequest == 0} { return } + + if {[catch { + set z1_request_data(pool) [LB::server] + if {$static::debug} { + log local0. "Selected pool member: $z1_request_data(pool)" + } + } err]} { + log local0.error "Error in LB_SELECTED: $err" + } + } + + ## Exit gracefully if this request is not marked for processing. + when HTTP_RESPONSE { + if { $processRequest == 0} { return } + if {[info exists z1_rate_allow] && $z1_rate_allow == 0} { return } + # Store all HTTP response data immediately to avoid timing issues + if {[catch { + set http_status [HTTP::status] + set res_content_type [HTTP::header "Content-Type"] + set res_content_length [expr {[HTTP::header "Content-Length"] ne "" ? [HTTP::header "Content-Length"] : 0}] + + if {$static::debug} { + log local0. "Processing HTTP response with status: $http_status" + } + # Build response headers JSON efficiently + if {[catch { + set header_parts [list] + foreach header_name [HTTP::header names] { + if {$header_name ne "Content-Type"} { + set header_value [HTTP::header $header_name] + lappend header_parts "\"$header_name\":\"[URI::encode $header_value]\"" + } + } + set response_headers "\{[join $header_parts ,]\}" + } err]} { + log local0.error "Error processing response headers: $err" + } + # Generate RFC 5424 compliant syslog message efficiently + if {[catch { + set timestamp [clock format [clock seconds] -format $static::timestamp_format -gmt true] + set types [join $z1_request_data(sensitive_data_types) ","] + # Properly format the sensitive data types + if {$static::debug} { + log local0. "Preparing final message with [llength $z1_request_data(sensitive_data_types)] sensitive data types" + } + set msg_base [format $static::json_template $z1_request_data(http_uri) $z1_request_data(http_host) $z1_request_data(http_method) $http_status] + # Build message parts list with previously stored data + set msg_parts [list] + lappend msg_parts "\"uri\":\"$z1_request_data(http_uri)\"" + lappend msg_parts "\"host\":\"$z1_request_data(http_host)\"" + lappend msg_parts "\"method\":\"$z1_request_data(http_method)\"" + lappend msg_parts "\"statusCode\":\"$http_status\"" + lappend msg_parts "\"xff\":\"$z1_request_data(xff)\"" + set reqCType [expr {[info exists z1_request_data(req_content_type)] ? $z1_request_data(req_content_type) : ""}] + lappend msg_parts "\"reqCType\":\"$reqCType\"" + lappend msg_parts "\"resCType\":\"$res_content_type\"" + lappend msg_parts "\"httpv\":\"$z1_request_data(http_version)\"" + lappend msg_parts "\"hasAuthorization\":$z1_request_data(has_auth)" + lappend msg_parts "\"sensitiveInHeaders\":$z1_request_data(sensitive_in_headers)" + lappend msg_parts "\"sensitiveDataTypes\":\"$types\"" + lappend msg_parts "\"sensitiveInPayload\":$z1_request_data(sensitive_in_payload)" + lappend msg_parts "\"payloadType\":\"$z1_request_data(payload_type)\"" + lappend msg_parts "\"vs_name\":\"$z1_request_data(virtual_server)\"" + lappend msg_parts "\"globalCounter\":$z1_global_counter" + # Construct the final message with proper JSON formatting + set msg_json "\{[join $msg_parts ,]\}" + set final_message "<118>1 $timestamp [info hostname] F5-API-Discovery - - - $msg_json\n" + + if {$static::debug} { + log local0. "Sending HSL message: $msg_json" + } + ## Send HSL log + if {[catch { + HSL::send $hsl $final_message + } err]} { + log local0.error "Error sending HSL message: $err" + } + } err]} { + log local0.error "Error preparing message: $err" + } + + if {$static::debug} { + log local0. "Request processing completed" + } + + if {[catch { + unset -nocomplain -- msg_parts msg_json final_message timestamp types header_parts + unset -nocomplain -- http_status res_content_type res_content_length response_headers + array unset z1_request_data + array unset z1_response_data + unset -nocomplain -- z1_payload + } err]} { + log local0.error "Error cleaning up variables: $err" + } + } err]} { + log local0.error "Error in HTTP_RESPONSE: $err" + } + } + +Attach the iRule to the Internal VS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Edit the API Application VS named ``lad-vs-sentence`` and attach the iRule on it + +.. image:: ../pictures/irule-vs2.png + :align: left + +* FINISHED + +.. note:: + + The BIG-IP is ready to receive traffic, and send API traffic datas to the LAD Server. \ No newline at end of file diff --git a/docs/class4/module4/lab3/lab3.rst b/docs/class4/module4/lab3/lab3.rst index 8ace77f..7086e2c 100644 --- a/docs/class4/module4/lab3/lab3.rst +++ b/docs/class4/module4/lab3/lab3.rst @@ -1,13 +1,429 @@ See API Discovery outcomes and block shadow APIs ================================================ -There are multiple options to do Rate Limiting in F5XC. In this lab, we are focusing on API Protection Rate Limiting. +After 2-3 minutes, you can start seeing insights into the LAD Console. -The goal is to rate limit an endpoint at risk because we discovered an attack or it is a shadow API and we are not sure if we should allow or block it. +Check what the console can display +---------------------------------- -Enable Rate Limiting from the Security Dashboard ------------------------------------------------- +* Click on the Dashboard menu and check the insights +* You an see 1 domain and several endpoints +.. image:: ../pictures/dashboard.png + :align: left + :scale: 50% + +* Click on API Endpoints and check the insights +* You can see for each disovered endpoints + + * PATH + * Method + * Sensitive data + * Authentication (if used) + * Category (Discovered, Shadow, Inventory) + * Risk score - you can click on it to get more details + * Status + +.. image:: ../pictures/api-endpoints.png + :align: left + :scale: 50% + +Import the OpenAPI spec file for this app +----------------------------------------- + +This applications called ``sentence app`` displays a sentence with several words. You can learn more here : https://github.com/f5devcentral/sentence-demo-app +On purpose, for this lab, one endpoint will not be part of the OAS Spec file. It is the ``/colors`` endpoint. + +The OAS spec file is below. + +* Copy the OAS spec file below and save it on your laptop as ``oas-sentence.yaml`` + +.. code-block:: YAML + + openapi: 3.0.1 + info: + title: API Sentence Generator + description: list of attributes + version: v1-auth + paths: + /api/adjectives: + get: + description: List all adjectives + operationId: listAdjectives + tags: + - adjectives + responses: + '200': + description: a list of adjectives with their index + content: + application/json: + schema: + $ref: "#/components/schemas/Adjectives" + #examples: {"adjectives": [{"id":1,"name":"blue"}]} + post: + description: create an adjective + operationId: createAdjective + tags: + - adjectives + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + id: + type: integer + examples: + '0': + value: '{"name":"calm"}' + '1': + value: '{"id":2}' + responses: + '201': + description: adjective created + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + delete: + description: delete an adjective + operationId: deleteAdjective + tags: + - adjectives + responses: + '200': + description: Delete an Adjective + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + /api/adjectives/{id}: + get: + description: get a specific adjective + operationId: showAdjectiveById + tags: + - adjectives + parameters: + - name: id + in: path + required: true + description: id of the adjective to retrieve + schema: + type: integer + responses: + '200': + description: Successful + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Adjective" + # examples: {"adjectives": [{"id":1,"name":"blue"}]} + delete: + description: delete an adjective + tags: + - adjectives + parameters: + - name: id + in: path + required: true + description: id of the adjective to retrieve + schema: + type: integer + responses: + '200': + description: Delete an Adjective + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + /api/animals: + get: + description: List all animals + operationId: listAnimals + tags: + - animals + responses: + '200': + description: a list of animals with their index + content: + application/json: + schema: + $ref: "#/components/schemas/Animals" + #examples: {"animals": [{"id":1,"name":"lion"}]} + post: + description: create an animal + operationId: createAnimal + tags: + - animals + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + id: + type: integer + examples: + '0': + value: '{"name":"cat"}' + '1': + value: '{"id":2}' + responses: + '201': + description: animal created + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + delete: + description: delete an adjective + operationId: deleteAnimal + tags: + - animals + responses: + '200': + description: Delete an Adjective + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + /api/animals/{id}: + get: + description: get a specific adjective + operationId: showAnimalsById + tags: + - animals + parameters: + - name: id + in: path + required: true + description: id of the animal to retrieve + schema: + type: integer + responses: + '200': + description: Successful + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Animal" + # examples: {"adjectives": [{"id":1,"name":"lion"}]} + delete: + description: delete an adjective + tags: + - animals + parameters: + - name: id + in: path + required: true + description: id of the animal to retrieve + schema: + type: integer + responses: + '200': + description: Delete an Adjective + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + /api/locations: + get: + description: List all locations + operationId: listlocations + security: + - bearerAuth: [] + tags: + - locations + responses: + '200': + description: a list of locations with their index + content: + application/json: + schema: + $ref: "#/components/schemas/Locations" + #examples: {"animals": [{"id":1,"name":"mountain"}]} + post: + description: create a location + operationId: createLocation + security: + - bearerAuth: [] + tags: + - locations + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + id: + type: integer + examples: + '0': + value: '{"name":"lake"}' + responses: + '201': + description: location created + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + delete: + description: delete a location + operationId: deleteLocation + security: + - bearerAuth: [] + tags: + - locations + responses: + '200': + description: Delete a Location + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + /api/locations/{id}: + get: + description: get a specific location + operationId: showLocationById + tags: + - locations + parameters: + - name: id + in: path + required: true + description: id of the location to retrieve + schema: + type: integer + responses: + '200': + description: Successful + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Location" + # examples: {"locations": [{"id":1,"name":"mountain"}]} + delete: + description: delete a location + tags: + - locations + parameters: + - name: id + in: path + required: true + description: id of the location to retrieve + schema: + type: integer + responses: + '200': + description: Delete a Location + content: + application/json; charset=utf-8: + schema: + type: string + examples: {} + + components: + schemas: + Adjective: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + # tag: + # type: string + Adjectives: + type: array + items: + $ref: "#/components/schemas/Adjective" + + Animal: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + # tag: + # type: string + Animals: + type: array + items: + $ref: "#/components/schemas/Animal" + + + Location: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + # tag: + # type: string + Locations: + type: array + items: + $ref: "#/components/schemas/Location" + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + servers: + - url: http://api.sentence.com/api + + +* Click on ``Upload Schema`` + +.. image:: ../pictures/click-oas.png + :align: left + :scale: 50% + +* Choose Choose File and upload your ``oas-sentence.yaml`` file. +* Warning : You must select the file, and then click ``Upload Selected`` + +.. image:: ../pictures/upload-oas.png + :align: left + :scale: 50% + +* You will see a summary. Close the windows and get back to the API Endpoint screen + +.. image:: ../pictures/close-oas.png + :align: left + :scale: 50% + +* Now, you can see the full insights categorized + + * Inventory : endpoints from the OAS spec file + * Discovered-Inventory : endpoints consumed (discovered) and also part of the OAS spec file + * Discovered-Shadow : endpoints consumed (discovered) and NOT part of the OAS spec file + +.. image:: ../pictures/outcomes.png + :align: left + :scale: 50% -Test your Rate Limiting config ------------------------------- diff --git a/docs/class4/module4/module4.rst b/docs/class4/module4/module4.rst index 1654404..8a9f1e5 100644 --- a/docs/class4/module4/module4.rst +++ b/docs/class4/module4/module4.rst @@ -1,7 +1,12 @@ Local API Discovery ################### -In this section, we will deploy the F5 Local API Discovery solution. This solution is an AirGapped API Discovery solution for BIG-IP and is disconnected from F5 Distributed Cloud. All datas, processing and logs are handled locally. +In this section, we will deploy the F5 Local API Discovery solution. + +Local API Discovery is a solution that helps you uncover and analyze API activity within your environment without relying on external connectivity or cloud-based services. It is ideal for deployments where data locality, privacy, or regulatory concerns demand self-managed API traffic inspection. +Local API Discovery collects and analyzes per-request HTTP metadata from BIG-IP systems. It detects both known and undocumented (shadow) API endpoints, tracks usage frequency, identifies sensitive data flows, and flags unauthenticated access points. All analysis and dashboards operate locally within your infrastructure whether on a physical server, virtual machine, or private cloud instance. + +Designed for security-sensitive or air-gapped environments, LAD empowers teams to gain actionable API visibility, manage schema governance, and take mitigation actions all without exposing data outside your trusted network boundary. .. image:: ./pictures/intro-lad.png :align: left diff --git a/docs/class4/module4/pictures/api-endpoints.png b/docs/class4/module4/pictures/api-endpoints.png new file mode 100644 index 0000000..1c1a079 Binary files /dev/null and b/docs/class4/module4/pictures/api-endpoints.png differ diff --git a/docs/class4/module4/pictures/click-oas.png b/docs/class4/module4/pictures/click-oas.png new file mode 100644 index 0000000..fd2679d Binary files /dev/null and b/docs/class4/module4/pictures/click-oas.png differ diff --git a/docs/class4/module4/pictures/close-oas.png b/docs/class4/module4/pictures/close-oas.png new file mode 100644 index 0000000..2f2f004 Binary files /dev/null and b/docs/class4/module4/pictures/close-oas.png differ diff --git a/docs/class4/module4/pictures/config-vs.png b/docs/class4/module4/pictures/config-vs.png new file mode 100644 index 0000000..5e8230c Binary files /dev/null and b/docs/class4/module4/pictures/config-vs.png differ diff --git a/docs/class4/module4/pictures/dashboard.png b/docs/class4/module4/pictures/dashboard.png new file mode 100644 index 0000000..291238d Binary files /dev/null and b/docs/class4/module4/pictures/dashboard.png differ diff --git a/docs/class4/module4/pictures/integration1.png b/docs/class4/module4/pictures/integration1.png new file mode 100644 index 0000000..a0fb19a Binary files /dev/null and b/docs/class4/module4/pictures/integration1.png differ diff --git a/docs/class4/module4/pictures/internal-pool.png b/docs/class4/module4/pictures/internal-pool.png new file mode 100644 index 0000000..6b0a1b8 Binary files /dev/null and b/docs/class4/module4/pictures/internal-pool.png differ diff --git a/docs/class4/module4/pictures/irule-vs2.png b/docs/class4/module4/pictures/irule-vs2.png new file mode 100644 index 0000000..198311f Binary files /dev/null and b/docs/class4/module4/pictures/irule-vs2.png differ diff --git a/docs/class4/module4/pictures/irule2.png b/docs/class4/module4/pictures/irule2.png new file mode 100644 index 0000000..1e35729 Binary files /dev/null and b/docs/class4/module4/pictures/irule2.png differ diff --git a/docs/class4/module4/pictures/lad-page.png b/docs/class4/module4/pictures/lad-page.png new file mode 100644 index 0000000..bb4d7d3 Binary files /dev/null and b/docs/class4/module4/pictures/lad-page.png differ diff --git a/docs/class4/module4/pictures/outcomes.png b/docs/class4/module4/pictures/outcomes.png new file mode 100644 index 0000000..e2c3a4c Binary files /dev/null and b/docs/class4/module4/pictures/outcomes.png differ diff --git a/docs/class4/module4/pictures/pool_list.png b/docs/class4/module4/pictures/pool_list.png new file mode 100644 index 0000000..550cba8 Binary files /dev/null and b/docs/class4/module4/pictures/pool_list.png differ diff --git a/docs/class4/module4/pictures/traffic-flow2.png b/docs/class4/module4/pictures/traffic-flow2.png new file mode 100644 index 0000000..1b4c366 Binary files /dev/null and b/docs/class4/module4/pictures/traffic-flow2.png differ diff --git a/docs/class4/module4/pictures/upload-oas.png b/docs/class4/module4/pictures/upload-oas.png new file mode 100644 index 0000000..164a45a Binary files /dev/null and b/docs/class4/module4/pictures/upload-oas.png differ