updating docs and package bits [ci skip]
@@ -22,8 +22,11 @@ tags:
|
||||
analytic_story:
|
||||
- AWS User Monitoring
|
||||
asset_type: AWS Account
|
||||
automated_detection_testing: passed
|
||||
cis20:
|
||||
- CIS 13
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1526/aws_security_scanner/aws_security_scanner.json
|
||||
kill_chain_phases:
|
||||
- Actions on Objectives
|
||||
mitre_attack_id:
|
||||
@@ -48,6 +51,3 @@ tags:
|
||||
risk_object_type: system
|
||||
risk_score: 20
|
||||
security_domain: network
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1526/aws_security_scanner/aws_security_scanner.json
|
||||
|
||||
@@ -9,8 +9,10 @@ datamodel:
|
||||
description: The following query identifies Microsoft Background Intelligent Transfer
|
||||
Service utility `bitsadmin.exe` scheduling a BITS job to persist on an endpoint.
|
||||
The query identifies the parameters used to create, resume or add a file to a BITS
|
||||
job. Typically seen combined in a oneliner or ran in sequence. If identified, review the BITS job created and capture any files written to disk. It is possible for BITS to be used to upload files and this may require further network data analysis to identify. You can use `bitsadmin /list /verbose`
|
||||
to list out the jobs during investigation.
|
||||
job. Typically seen combined in a oneliner or ran in sequence. If identified, review
|
||||
the BITS job created and capture any files written to disk. It is possible for BITS
|
||||
to be used to upload files and this may require further network data analysis to
|
||||
identify. You can use `bitsadmin /list /verbose` to list out the jobs during investigation.
|
||||
search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time)
|
||||
as lastTime from datamodel=Endpoint.Processes where Processes.process_name=bitsadmin.exe
|
||||
Processes.process IN (*create*, *addfile*, *setnotifyflags*, *setnotifycmdline*,
|
||||
@@ -32,6 +34,7 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- BITS Jobs
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1197/atomic_red_team/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
@@ -50,4 +53,3 @@ tags:
|
||||
- Processes.user
|
||||
- Processes.dest
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
|
||||
@@ -38,6 +38,7 @@ tags:
|
||||
analytic_story:
|
||||
- Ingress Tool Transfer
|
||||
- BITS Jobs
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1197/atomic_red_team/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
@@ -57,4 +58,3 @@ tags:
|
||||
- Processes.user
|
||||
- Processes.dest
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
|
||||
@@ -26,6 +26,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -42,8 +47,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -28,6 +28,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -45,8 +50,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_nam
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -28,6 +28,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -44,8 +49,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -26,6 +26,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -42,8 +47,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_nam
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -25,6 +25,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -41,8 +46,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -24,6 +24,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -40,8 +45,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -26,6 +26,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -45,8 +50,3 @@ tags:
|
||||
- Processes.process_id
|
||||
- Processes.parent_process_id
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -26,6 +26,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -42,8 +47,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -27,6 +27,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -43,8 +48,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -27,6 +27,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -43,8 +48,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -26,6 +26,11 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Windows Defense Evasion Tactics
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -42,8 +47,3 @@ tags:
|
||||
- Registry.dest
|
||||
- Registry.registry_value_name
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-security.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-system.log
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1562.001/win_app_defender_disabling/windows-sysmon.log
|
||||
|
||||
@@ -10,7 +10,7 @@ description: 'The following analytic identifies "dsquery.exe" execution with arg
|
||||
looking for `TrustedDomain` query directly on the command-line. This is typically
|
||||
indicative of an Administrator or adversary perform domain trust discovery. Note
|
||||
that this query does not identify any other variations of "Dsquery.exe" usage.\
|
||||
|
||||
|
||||
Within this detection, it is assumed `dsquery.exe` is not moved or renamed.\
|
||||
|
||||
The search will return the first time and last time these command-line arguments
|
||||
@@ -46,6 +46,7 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Domain Trust Discovery
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1482/atomic_red_team/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
@@ -63,4 +64,3 @@ tags:
|
||||
- Processes.user
|
||||
- Processes.dest
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
|
||||
@@ -26,6 +26,7 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- Malicious Powershell
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1569.002/atomic_red_team/windows-system.log
|
||||
kill_chain_phases:
|
||||
@@ -46,4 +47,3 @@ tags:
|
||||
- Service_Account
|
||||
- user
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
|
||||
@@ -31,6 +31,7 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- BITS Jobs
|
||||
automated_detection_testing: passed
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1197/atomic_red_team/windows-sysmon.log
|
||||
kill_chain_phases:
|
||||
@@ -49,4 +50,3 @@ tags:
|
||||
- Processes.user
|
||||
- Processes.dest
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
|
||||
@@ -284,6 +284,50 @@ _version_: 1
|
||||
<details>
|
||||
<summary>details</summary>
|
||||
|
||||
### BITS Jobs
|
||||
Adversaries may abuse BITS jobs to persistently execute or clean up after malicious payloads.
|
||||
|
||||
- **Product**: Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
- **Datamodel**: Endpoint
|
||||
- **ATT&CK**: [T1105](https://attack.mitre.org/techniques/T1105/), [T1197](https://attack.mitre.org/techniques/T1197/)
|
||||
- **Last Updated**: 2021-03-26
|
||||
|
||||
<details>
|
||||
<summary>details</summary>
|
||||
|
||||
#### Detection Profile
|
||||
|
||||
* [BITS Job Persistence](detections.md#bits-job-persistence)
|
||||
|
||||
* [BITSAdmin Download File](detections.md#bitsadmin-download-file)
|
||||
|
||||
* [PowerShell Start-BitsTransfer](detections.md#powershell-start-bitstransfer)
|
||||
|
||||
|
||||
#### ATT&CK
|
||||
|
||||
| ID | Technique | Tactic |
|
||||
| ----------- | ----------- |--------------|
|
||||
| T1197 | BITS Jobs | Defense Evasion, Persistence |
|
||||
| T1105 | Ingress Tool Transfer | Command and Control |
|
||||
|
||||
#### Kill Chain Phase
|
||||
|
||||
* Exploitation
|
||||
|
||||
|
||||
#### Reference
|
||||
|
||||
* https://attack.mitre.org/techniques/T1197/
|
||||
|
||||
* https://docs.microsoft.com/en-us/windows/win32/bits/bitsadmin-tool
|
||||
|
||||
|
||||
_version_: 1
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Baron Samedit CVE-2021-3156
|
||||
Uncover activity consistent with CVE-2021-3156. Discovered by the Qualys Research Team, this vulnerability has been found to affect sudo across multiple Linux distributions (Ubuntu 20.04 and prior, Debian 10 and prior, Fedora 33 and prior). As this vulnerability was committed to code in July 2011, there will be many distributions affected. Successful exploitation of this vulnerability allows any unprivileged user to gain root privileges on the vulnerable host.
|
||||
|
||||
@@ -924,6 +968,48 @@ _version_: 2
|
||||
|
||||
---
|
||||
|
||||
### Domain Trust Discovery
|
||||
Adversaries may attempt to gather information on domain trust relationships that may be used to identify lateral movement opportunities in Windows multi-domain/forest environments.
|
||||
|
||||
- **Product**: Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
- **Datamodel**: Endpoint
|
||||
- **ATT&CK**: [T1018](https://attack.mitre.org/techniques/T1018/), [T1482](https://attack.mitre.org/techniques/T1482/)
|
||||
- **Last Updated**: 2021-03-25
|
||||
|
||||
<details>
|
||||
<summary>details</summary>
|
||||
|
||||
#### Detection Profile
|
||||
|
||||
* [DSQuery Domain Discovery](detections.md#dsquery-domain-discovery)
|
||||
|
||||
* [NLTest Domain Trust Discovery](detections.md#nltest-domain-trust-discovery)
|
||||
|
||||
* [Windows AdFind Exe](detections.md#windows-adfind-exe)
|
||||
|
||||
|
||||
#### ATT&CK
|
||||
|
||||
| ID | Technique | Tactic |
|
||||
| ----------- | ----------- |--------------|
|
||||
| T1482 | Domain Trust Discovery | Discovery |
|
||||
| T1018 | Remote System Discovery | Discovery |
|
||||
|
||||
#### Kill Chain Phase
|
||||
|
||||
* Exploitation
|
||||
|
||||
|
||||
#### Reference
|
||||
|
||||
* https://attack.mitre.org/techniques/T1482/
|
||||
|
||||
|
||||
_version_: 1
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### F5 TMUI RCE CVE-2020-5902
|
||||
Uncover activity consistent with CVE-2020-5902. Discovered by Positive Technologies researchers, this vulnerability affects F5 BIG-IP, BIG-IQ. and Traffix SDC devices (vulnerable versions in F5 support link below). This vulnerability allows unauthenticated users, along with authenticated users, who have access to the configuration utility to execute system commands, create/delete files, disable services, and/or execute Java code. This vulnerability can result in full system compromise.
|
||||
|
||||
@@ -1054,7 +1140,7 @@ Adversaries may transfer tools or other files from an external system into a com
|
||||
|
||||
- **Product**: Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
- **Datamodel**: Endpoint
|
||||
- **ATT&CK**: [T1105](https://attack.mitre.org/techniques/T1105/)
|
||||
- **ATT&CK**: [T1059.001](https://attack.mitre.org/techniques/T1059.001/), [T1105](https://attack.mitre.org/techniques/T1105/), [T1197](https://attack.mitre.org/techniques/T1197/)
|
||||
- **Last Updated**: 2021-03-24
|
||||
|
||||
<details>
|
||||
@@ -1062,6 +1148,12 @@ Adversaries may transfer tools or other files from an external system into a com
|
||||
|
||||
#### Detection Profile
|
||||
|
||||
* [Any Powershell DownloadFile](detections.md#any-powershell-downloadfile)
|
||||
|
||||
* [Any Powershell DownloadString](detections.md#any-powershell-downloadstring)
|
||||
|
||||
* [BITSAdmin Download File](detections.md#bitsadmin-download-file)
|
||||
|
||||
* [CertUtil Download With URLCache and Split Arguments](detections.md#certutil-download-with-urlcache-and-split-arguments)
|
||||
|
||||
* [CertUtil Download With VerifyCtl and Split Arguments](detections.md#certutil-download-with-verifyctl-and-split-arguments)
|
||||
@@ -1073,7 +1165,21 @@ Adversaries may transfer tools or other files from an external system into a com
|
||||
|
||||
| ID | Technique | Tactic |
|
||||
| ----------- | ----------- |--------------|
|
||||
| T1059.001 | PowerShell | Execution |
|
||||
| T1197 | BITS Jobs | Defense Evasion, Persistence |
|
||||
| T1105 | Ingress Tool Transfer | Command and Control |
|
||||
| T1003 | OS Credential Dumping | Credential Access |
|
||||
| T1021 | Remote Services | Lateral Movement |
|
||||
| T1113 | Screen Capture | Collection |
|
||||
| T1123 | Audio Capture | Collection |
|
||||
| T1563 | Remote Service Session Hijacking | Lateral Movement |
|
||||
| T1053 | Scheduled Task/Job | Execution, Persistence, Privilege Escalation |
|
||||
| T1134 | Access Token Manipulation | Defense Evasion, Privilege Escalation |
|
||||
| T1548 | Abuse Elevation Control Mechanism | Defense Evasion, Privilege Escalation |
|
||||
| T1055 | Process Injection | Defense Evasion, Privilege Escalation |
|
||||
| T1106 | Native API | Execution |
|
||||
| T1569 | System Services | Execution |
|
||||
| T1027 | Obfuscated Files or Information | Defense Evasion |
|
||||
|
||||
#### Kill Chain Phase
|
||||
|
||||
@@ -1189,6 +1295,8 @@ Attackers are finding stealthy ways "live off the land," leveraging utilities an
|
||||
| ID | Technique | Tactic |
|
||||
| ----------- | ----------- |--------------|
|
||||
| T1059.001 | PowerShell | Execution |
|
||||
| T1197 | BITS Jobs | Defense Evasion, Persistence |
|
||||
| T1105 | Ingress Tool Transfer | Command and Control |
|
||||
| T1003 | OS Credential Dumping | Credential Access |
|
||||
| T1021 | Remote Services | Lateral Movement |
|
||||
| T1113 | Screen Capture | Collection |
|
||||
@@ -2042,16 +2150,12 @@ Monitor and detect registry changes initiated from remote locations, which can b
|
||||
| ID | Technique | Tactic |
|
||||
| ----------- | ----------- |--------------|
|
||||
| T1548.002 | Bypass User Account Control | Defense Evasion, Privilege Escalation |
|
||||
| T1112 | Modify Registry | Defense Evasion |
|
||||
| T1222.001 | Windows File and Directory Permissions Modification | Defense Evasion |
|
||||
| T1547.010 | Port Monitors | Persistence, Privilege Escalation |
|
||||
| T1564.001 | Hidden Files and Directories | Defense Evasion |
|
||||
| T1547.001 | Registry Run Keys / Startup Folder | Persistence, Privilege Escalation |
|
||||
| T1546.012 | Image File Execution Options Injection | Persistence, Privilege Escalation |
|
||||
| T1546.011 | Application Shimming | Persistence, Privilege Escalation |
|
||||
| T1546.001 | Change Default File Association | Persistence, Privilege Escalation |
|
||||
| T1036 | Masquerading | Defense Evasion |
|
||||
| T1562.001 | Disable or Modify Tools | Defense Evasion |
|
||||
|
||||
#### Kill Chain Phase
|
||||
|
||||
@@ -2267,8 +2371,30 @@ Detect tactics used by malware to evade defenses on Windows endpoints. A few of
|
||||
|
||||
#### Detection Profile
|
||||
|
||||
* [Disable Registry Tool](detections.md#disable-registry-tool)
|
||||
|
||||
* [Disable Show Hidden Files](detections.md#disable-show-hidden-files)
|
||||
|
||||
* [Disable Windows Behavior Monitoring](detections.md#disable-windows-behavior-monitoring)
|
||||
|
||||
* [Disable Windows SmartScreen Protection](detections.md#disable-windows-smartscreen-protection)
|
||||
|
||||
* [Disabling CMD Application](detections.md#disabling-cmd-application)
|
||||
|
||||
* [Disabling ControlPanel](detections.md#disabling-controlpanel)
|
||||
|
||||
* [Disabling Firewall with Netsh](detections.md#disabling-firewall-with-netsh)
|
||||
|
||||
* [Disabling FolderOptions Windows Feature](detections.md#disabling-folderoptions-windows-feature)
|
||||
|
||||
* [Disabling NoRun Windows App](detections.md#disabling-norun-windows-app)
|
||||
|
||||
* [Disabling Remote User Account Control](detections.md#disabling-remote-user-account-control)
|
||||
|
||||
* [Disabling SystemRestore In Registry](detections.md#disabling-systemrestore-in-registry)
|
||||
|
||||
* [Disabling Task Manager](detections.md#disabling-task-manager)
|
||||
|
||||
* [Eventvwr UAC Bypass](detections.md#eventvwr-uac-bypass)
|
||||
|
||||
* [FodHelper UAC Bypass](detections.md#fodhelper-uac-bypass)
|
||||
@@ -2290,17 +2416,12 @@ Detect tactics used by malware to evade defenses on Windows endpoints. A few of
|
||||
|
||||
| ID | Technique | Tactic |
|
||||
| ----------- | ----------- |--------------|
|
||||
| T1562.001 | Disable or Modify Tools | Defense Evasion |
|
||||
| T1564.001 | Hidden Files and Directories | Defense Evasion |
|
||||
| T1548.002 | Bypass User Account Control | Defense Evasion, Privilege Escalation |
|
||||
| T1112 | Modify Registry | Defense Evasion |
|
||||
| T1222.001 | Windows File and Directory Permissions Modification | Defense Evasion |
|
||||
| T1547.010 | Port Monitors | Persistence, Privilege Escalation |
|
||||
| T1564.001 | Hidden Files and Directories | Defense Evasion |
|
||||
| T1547.001 | Registry Run Keys / Startup Folder | Persistence, Privilege Escalation |
|
||||
| T1546.012 | Image File Execution Options Injection | Persistence, Privilege Escalation |
|
||||
| T1546.011 | Application Shimming | Persistence, Privilege Escalation |
|
||||
| T1546.001 | Change Default File Association | Persistence, Privilege Escalation |
|
||||
| T1036 | Masquerading | Defense Evasion |
|
||||
| T1562.001 | Disable or Modify Tools | Defense Evasion |
|
||||
|
||||
#### Kill Chain Phase
|
||||
|
||||
@@ -3222,7 +3343,7 @@ Detect and investigate dormant user accounts for your AWS environment that have
|
||||
|
||||
- **Product**: Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
- **Datamodel**:
|
||||
- **ATT&CK**: [T1078.004](https://attack.mitre.org/techniques/T1078.004/)
|
||||
- **ATT&CK**: [T1078.004](https://attack.mitre.org/techniques/T1078.004/), [T1526](https://attack.mitre.org/techniques/T1526/)
|
||||
- **Last Updated**: 2018-03-12
|
||||
|
||||
<details>
|
||||
@@ -3230,6 +3351,8 @@ Detect and investigate dormant user accounts for your AWS environment that have
|
||||
|
||||
#### Detection Profile
|
||||
|
||||
* [AWS Excessive Security Scanning](detections.md#aws-excessive-security-scanning)
|
||||
|
||||
* [Detect API activity from users without MFA](detections.md#detect-api-activity-from-users-without-mfa)
|
||||
|
||||
* [Detect AWS API Activities From Unapproved Accounts](detections.md#detect-aws-api-activities-from-unapproved-accounts)
|
||||
@@ -3245,6 +3368,7 @@ Detect and investigate dormant user accounts for your AWS environment that have
|
||||
|
||||
| ID | Technique | Tactic |
|
||||
| ----------- | ----------- |--------------|
|
||||
| T1526 | Cloud Service Discovery | Discovery |
|
||||
| T1078.004 | Cloud Accounts | Defense Evasion, Initial Access, Persistence, Privilege Escalation |
|
||||
|
||||
#### Kill Chain Phase
|
||||
@@ -3990,7 +4114,7 @@ _version_: 1
|
||||
### Suspicious Cloud User Activities
|
||||
Detect and investigate suspicious activities by users and roles in your cloud environments.
|
||||
|
||||
- **Product**: Splunk Security Analytics for AWS, Splunk Security Analytics for AWS, Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
- **Product**: Splunk Security Analytics for AWS, Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
- **Datamodel**: Change
|
||||
- **ATT&CK**: [T1078](https://attack.mitre.org/techniques/T1078/), [T1078.004](https://attack.mitre.org/techniques/T1078.004/)
|
||||
- **Last Updated**: 2020-09-04
|
||||
|
||||
@@ -346,6 +346,61 @@ Monitor your environment for activity consistent with common attack techniques b
|
||||
==Adversary Tactics==
|
||||
|
||||
|
||||
===Bits jobs===
|
||||
Adversaries may abuse BITS jobs to persistently execute or clean up after malicious payloads.
|
||||
|
||||
* '''Product''': Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''': Endpoint
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1197/ T1197], [https://attack.mitre.org/techniques/T1105/ T1105]
|
||||
* '''Last Updated''': 2021-03-26
|
||||
|
||||
<div class="toccolours mw-collapsible mw-collapsed">
|
||||
<div class="mw-collapsible-content">
|
||||
|
||||
====Detection Profile====
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Bits_job_persistence|BITS Job Persistence]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Bitsadmin_download_file|BITSAdmin Download File]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Powershell_start-bitstransfer|PowerShell Start-BitsTransfer]]
|
||||
|
||||
|
||||
|
||||
====ATT&CK====
|
||||
{|
|
||||
! style="text-align:left;"| ID
|
||||
! Technique
|
||||
! Tactic
|
||||
|-
|
||||
| T1197
|
||||
| BITS Jobs
|
||||
| Defense Evasion, Persistence
|
||||
|-
|
||||
| T1105
|
||||
| Ingress Tool Transfer
|
||||
| Command and Control
|
||||
|}
|
||||
|
||||
|
||||
====Kill Chain Phase====
|
||||
|
||||
* Exploitation
|
||||
|
||||
|
||||
====Reference====
|
||||
|
||||
* https://attack.mitre.org/techniques/T1197/
|
||||
|
||||
* https://docs.microsoft.com/en-us/windows/win32/bits/bitsadmin-tool
|
||||
|
||||
|
||||
''version'': 1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
----
|
||||
|
||||
===Baron samedit cve-2021-3156===
|
||||
Uncover activity consistent with CVE-2021-3156. Discovered by the Qualys Research Team, this vulnerability has been found to affect sudo across multiple Linux distributions (Ubuntu 20.04 and prior, Debian 10 and prior, Fedora 33 and prior). As this vulnerability was committed to code in July 2011, there will be many distributions affected. Successful exploitation of this vulnerability allows any unprivileged user to gain root privileges on the vulnerable host.
|
||||
|
||||
@@ -1203,6 +1258,59 @@ Looks for activities and techniques associated with the disabling of security to
|
||||
|
||||
----
|
||||
|
||||
===Domain trust discovery===
|
||||
Adversaries may attempt to gather information on domain trust relationships that may be used to identify lateral movement opportunities in Windows multi-domain/forest environments.
|
||||
|
||||
* '''Product''': Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''': Endpoint
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1482/ T1482], [https://attack.mitre.org/techniques/T1018/ T1018]
|
||||
* '''Last Updated''': 2021-03-25
|
||||
|
||||
<div class="toccolours mw-collapsible mw-collapsed">
|
||||
<div class="mw-collapsible-content">
|
||||
|
||||
====Detection Profile====
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Dsquery_domain_discovery|DSQuery Domain Discovery]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Nltest_domain_trust_discovery|NLTest Domain Trust Discovery]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Windows_adfind_exe|Windows AdFind Exe]]
|
||||
|
||||
|
||||
|
||||
====ATT&CK====
|
||||
{|
|
||||
! style="text-align:left;"| ID
|
||||
! Technique
|
||||
! Tactic
|
||||
|-
|
||||
| T1482
|
||||
| Domain Trust Discovery
|
||||
| Discovery
|
||||
|-
|
||||
| T1018
|
||||
| Remote System Discovery
|
||||
| Discovery
|
||||
|}
|
||||
|
||||
|
||||
====Kill Chain Phase====
|
||||
|
||||
* Exploitation
|
||||
|
||||
|
||||
====Reference====
|
||||
|
||||
* https://attack.mitre.org/techniques/T1482/
|
||||
|
||||
|
||||
''version'': 1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
----
|
||||
|
||||
===F5 tmui rce cve-2020-5902===
|
||||
Uncover activity consistent with CVE-2020-5902. Discovered by Positive Technologies researchers, this vulnerability affects F5 BIG-IP, BIG-IQ. and Traffix SDC devices (vulnerable versions in F5 support link below). This vulnerability allows unauthenticated users, along with authenticated users, who have access to the configuration utility to execute system commands, create/delete files, disable services, and/or execute Java code. This vulnerability can result in full system compromise.
|
||||
|
||||
@@ -1370,7 +1478,7 @@ Adversaries may transfer tools or other files from an external system into a com
|
||||
|
||||
* '''Product''': Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''': Endpoint
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1105/ T1105]
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1059.001/ T1059.001], [https://attack.mitre.org/techniques/T1197/ T1197], [https://attack.mitre.org/techniques/T1105/ T1105], [https://attack.mitre.org/techniques/T1003/ T1003], [https://attack.mitre.org/techniques/T1021/ T1021], [https://attack.mitre.org/techniques/T1113/ T1113], [https://attack.mitre.org/techniques/T1123/ T1123], [https://attack.mitre.org/techniques/T1563/ T1563], [https://attack.mitre.org/techniques/T1053/ T1053], [https://attack.mitre.org/techniques/T1134/ T1134], [https://attack.mitre.org/techniques/T1548/ T1548], [https://attack.mitre.org/techniques/T1055/ T1055], [https://attack.mitre.org/techniques/T1106/ T1106], [https://attack.mitre.org/techniques/T1569/ T1569], [https://attack.mitre.org/techniques/T1027/ T1027]
|
||||
* '''Last Updated''': 2021-03-24
|
||||
|
||||
<div class="toccolours mw-collapsible mw-collapsed">
|
||||
@@ -1378,6 +1486,12 @@ Adversaries may transfer tools or other files from an external system into a com
|
||||
|
||||
====Detection Profile====
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Any_powershell_downloadfile|Any Powershell DownloadFile]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Any_powershell_downloadstring|Any Powershell DownloadString]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Bitsadmin_download_file|BITSAdmin Download File]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Certutil_download_with_urlcache_and_split_arguments|CertUtil Download With URLCache and Split Arguments]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Certutil_download_with_verifyctl_and_split_arguments|CertUtil Download With VerifyCtl and Split Arguments]]
|
||||
@@ -1392,9 +1506,65 @@ Adversaries may transfer tools or other files from an external system into a com
|
||||
! Technique
|
||||
! Tactic
|
||||
|-
|
||||
| T1059.001
|
||||
| PowerShell
|
||||
| Execution
|
||||
|-
|
||||
| T1197
|
||||
| BITS Jobs
|
||||
| Defense Evasion, Persistence
|
||||
|-
|
||||
| T1105
|
||||
| Ingress Tool Transfer
|
||||
| Command and Control
|
||||
|-
|
||||
| T1003
|
||||
| OS Credential Dumping
|
||||
| Credential Access
|
||||
|-
|
||||
| T1021
|
||||
| Remote Services
|
||||
| Lateral Movement
|
||||
|-
|
||||
| T1113
|
||||
| Screen Capture
|
||||
| Collection
|
||||
|-
|
||||
| T1123
|
||||
| Audio Capture
|
||||
| Collection
|
||||
|-
|
||||
| T1563
|
||||
| Remote Service Session Hijacking
|
||||
| Lateral Movement
|
||||
|-
|
||||
| T1053
|
||||
| Scheduled Task/Job
|
||||
| Execution, Persistence, Privilege Escalation
|
||||
|-
|
||||
| T1134
|
||||
| Access Token Manipulation
|
||||
| Defense Evasion, Privilege Escalation
|
||||
|-
|
||||
| T1548
|
||||
| Abuse Elevation Control Mechanism
|
||||
| Defense Evasion, Privilege Escalation
|
||||
|-
|
||||
| T1055
|
||||
| Process Injection
|
||||
| Defense Evasion, Privilege Escalation
|
||||
|-
|
||||
| T1106
|
||||
| Native API
|
||||
| Execution
|
||||
|-
|
||||
| T1569
|
||||
| System Services
|
||||
| Execution
|
||||
|-
|
||||
| T1027
|
||||
| Obfuscated Files or Information
|
||||
| Defense Evasion
|
||||
|}
|
||||
|
||||
|
||||
@@ -1488,7 +1658,7 @@ Attackers are finding stealthy ways "live off the land," leveraging utilities an
|
||||
|
||||
* '''Product''': Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''': Endpoint
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1059.001/ T1059.001], [https://attack.mitre.org/techniques/T1003/ T1003], [https://attack.mitre.org/techniques/T1021/ T1021], [https://attack.mitre.org/techniques/T1113/ T1113], [https://attack.mitre.org/techniques/T1123/ T1123], [https://attack.mitre.org/techniques/T1563/ T1563], [https://attack.mitre.org/techniques/T1053/ T1053], [https://attack.mitre.org/techniques/T1134/ T1134], [https://attack.mitre.org/techniques/T1548/ T1548], [https://attack.mitre.org/techniques/T1055/ T1055], [https://attack.mitre.org/techniques/T1106/ T1106], [https://attack.mitre.org/techniques/T1569/ T1569], [https://attack.mitre.org/techniques/T1027/ T1027]
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1059.001/ T1059.001], [https://attack.mitre.org/techniques/T1197/ T1197], [https://attack.mitre.org/techniques/T1105/ T1105], [https://attack.mitre.org/techniques/T1003/ T1003], [https://attack.mitre.org/techniques/T1021/ T1021], [https://attack.mitre.org/techniques/T1113/ T1113], [https://attack.mitre.org/techniques/T1123/ T1123], [https://attack.mitre.org/techniques/T1563/ T1563], [https://attack.mitre.org/techniques/T1053/ T1053], [https://attack.mitre.org/techniques/T1134/ T1134], [https://attack.mitre.org/techniques/T1548/ T1548], [https://attack.mitre.org/techniques/T1055/ T1055], [https://attack.mitre.org/techniques/T1106/ T1106], [https://attack.mitre.org/techniques/T1569/ T1569], [https://attack.mitre.org/techniques/T1027/ T1027]
|
||||
* '''Last Updated''': 2017-08-23
|
||||
|
||||
<div class="toccolours mw-collapsible mw-collapsed">
|
||||
@@ -1536,6 +1706,14 @@ Attackers are finding stealthy ways "live off the land," leveraging utilities an
|
||||
| PowerShell
|
||||
| Execution
|
||||
|-
|
||||
| T1197
|
||||
| BITS Jobs
|
||||
| Defense Evasion, Persistence
|
||||
|-
|
||||
| T1105
|
||||
| Ingress Tool Transfer
|
||||
| Command and Control
|
||||
|-
|
||||
| T1003
|
||||
| OS Credential Dumping
|
||||
| Credential Access
|
||||
@@ -2610,7 +2788,7 @@ Monitor and detect registry changes initiated from remote locations, which can b
|
||||
|
||||
* '''Product''': Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''': Endpoint
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1548.002/ T1548.002], [https://attack.mitre.org/techniques/T1112/ T1112], [https://attack.mitre.org/techniques/T1222.001/ T1222.001], [https://attack.mitre.org/techniques/T1547.010/ T1547.010], [https://attack.mitre.org/techniques/T1564.001/ T1564.001], [https://attack.mitre.org/techniques/T1547.001/ T1547.001], [https://attack.mitre.org/techniques/T1546.012/ T1546.012], [https://attack.mitre.org/techniques/T1546.011/ T1546.011], [https://attack.mitre.org/techniques/T1546.001/ T1546.001], [https://attack.mitre.org/techniques/T1036/ T1036], [https://attack.mitre.org/techniques/T1562.001/ T1562.001]
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1548.002/ T1548.002], [https://attack.mitre.org/techniques/T1547.010/ T1547.010], [https://attack.mitre.org/techniques/T1564.001/ T1564.001], [https://attack.mitre.org/techniques/T1547.001/ T1547.001], [https://attack.mitre.org/techniques/T1546.012/ T1546.012], [https://attack.mitre.org/techniques/T1546.011/ T1546.011], [https://attack.mitre.org/techniques/T1546.001/ T1546.001]
|
||||
* '''Last Updated''': 2018-05-31
|
||||
|
||||
<div class="toccolours mw-collapsible mw-collapsed">
|
||||
@@ -2646,14 +2824,6 @@ Monitor and detect registry changes initiated from remote locations, which can b
|
||||
| Bypass User Account Control
|
||||
| Defense Evasion, Privilege Escalation
|
||||
|-
|
||||
| T1112
|
||||
| Modify Registry
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1222.001
|
||||
| Windows File and Directory Permissions Modification
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1547.010
|
||||
| Port Monitors
|
||||
| Persistence, Privilege Escalation
|
||||
@@ -2677,14 +2847,6 @@ Monitor and detect registry changes initiated from remote locations, which can b
|
||||
| T1546.001
|
||||
| Change Default File Association
|
||||
| Persistence, Privilege Escalation
|
||||
|-
|
||||
| T1036
|
||||
| Masquerading
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1562.001
|
||||
| Disable or Modify Tools
|
||||
| Defense Evasion
|
||||
|}
|
||||
|
||||
|
||||
@@ -2945,7 +3107,7 @@ Detect tactics used by malware to evade defenses on Windows endpoints. A few of
|
||||
|
||||
* '''Product''': Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''': Endpoint
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1548.002/ T1548.002], [https://attack.mitre.org/techniques/T1112/ T1112], [https://attack.mitre.org/techniques/T1222.001/ T1222.001], [https://attack.mitre.org/techniques/T1547.010/ T1547.010], [https://attack.mitre.org/techniques/T1564.001/ T1564.001], [https://attack.mitre.org/techniques/T1547.001/ T1547.001], [https://attack.mitre.org/techniques/T1546.012/ T1546.012], [https://attack.mitre.org/techniques/T1546.011/ T1546.011], [https://attack.mitre.org/techniques/T1546.001/ T1546.001], [https://attack.mitre.org/techniques/T1036/ T1036], [https://attack.mitre.org/techniques/T1562.001/ T1562.001]
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1562.001/ T1562.001], [https://attack.mitre.org/techniques/T1564.001/ T1564.001], [https://attack.mitre.org/techniques/T1548.002/ T1548.002], [https://attack.mitre.org/techniques/T1112/ T1112], [https://attack.mitre.org/techniques/T1222.001/ T1222.001], [https://attack.mitre.org/techniques/T1036/ T1036]
|
||||
* '''Last Updated''': 2018-05-31
|
||||
|
||||
<div class="toccolours mw-collapsible mw-collapsed">
|
||||
@@ -2953,8 +3115,30 @@ Detect tactics used by malware to evade defenses on Windows endpoints. A few of
|
||||
|
||||
====Detection Profile====
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disable_registry_tool|Disable Registry Tool]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disable_show_hidden_files|Disable Show Hidden Files]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disable_windows_behavior_monitoring|Disable Windows Behavior Monitoring]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disable_windows_smartscreen_protection|Disable Windows SmartScreen Protection]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_cmd_application|Disabling CMD Application]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_controlpanel|Disabling ControlPanel]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_firewall_with_netsh|Disabling Firewall with Netsh]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_folderoptions_windows_feature|Disabling FolderOptions Windows Feature]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_norun_windows_app|Disabling NoRun Windows App]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_remote_user_account_control|Disabling Remote User Account Control]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_systemrestore_in_registry|Disabling SystemRestore In Registry]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Disabling_task_manager|Disabling Task Manager]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Eventvwr_uac_bypass|Eventvwr UAC Bypass]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Fodhelper_uac_bypass|FodHelper UAC Bypass]]
|
||||
@@ -2979,6 +3163,14 @@ Detect tactics used by malware to evade defenses on Windows endpoints. A few of
|
||||
! Technique
|
||||
! Tactic
|
||||
|-
|
||||
| T1562.001
|
||||
| Disable or Modify Tools
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1564.001
|
||||
| Hidden Files and Directories
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1548.002
|
||||
| Bypass User Account Control
|
||||
| Defense Evasion, Privilege Escalation
|
||||
@@ -2991,37 +3183,9 @@ Detect tactics used by malware to evade defenses on Windows endpoints. A few of
|
||||
| Windows File and Directory Permissions Modification
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1547.010
|
||||
| Port Monitors
|
||||
| Persistence, Privilege Escalation
|
||||
|-
|
||||
| T1564.001
|
||||
| Hidden Files and Directories
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1547.001
|
||||
| Registry Run Keys / Startup Folder
|
||||
| Persistence, Privilege Escalation
|
||||
|-
|
||||
| T1546.012
|
||||
| Image File Execution Options Injection
|
||||
| Persistence, Privilege Escalation
|
||||
|-
|
||||
| T1546.011
|
||||
| Application Shimming
|
||||
| Persistence, Privilege Escalation
|
||||
|-
|
||||
| T1546.001
|
||||
| Change Default File Association
|
||||
| Persistence, Privilege Escalation
|
||||
|-
|
||||
| T1036
|
||||
| Masquerading
|
||||
| Defense Evasion
|
||||
|-
|
||||
| T1562.001
|
||||
| Disable or Modify Tools
|
||||
| Defense Evasion
|
||||
|}
|
||||
|
||||
|
||||
@@ -4221,7 +4385,7 @@ Detect and investigate dormant user accounts for your AWS environment that have
|
||||
|
||||
* '''Product''': Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''':
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1078.004/ T1078.004]
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1526/ T1526], [https://attack.mitre.org/techniques/T1078.004/ T1078.004]
|
||||
* '''Last Updated''': 2018-03-12
|
||||
|
||||
<div class="toccolours mw-collapsible mw-collapsed">
|
||||
@@ -4229,6 +4393,8 @@ Detect and investigate dormant user accounts for your AWS environment that have
|
||||
|
||||
====Detection Profile====
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Aws_excessive_security_scanning|AWS Excessive Security Scanning]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Detect_api_activity_from_users_without_mfa|Detect API activity from users without MFA]]
|
||||
|
||||
* [[Documentation:ESSOC:detections:Detections#Detect_aws_api_activities_from_unapproved_accounts|Detect AWS API Activities From Unapproved Accounts]]
|
||||
@@ -4247,6 +4413,10 @@ Detect and investigate dormant user accounts for your AWS environment that have
|
||||
! Technique
|
||||
! Tactic
|
||||
|-
|
||||
| T1526
|
||||
| Cloud Service Discovery
|
||||
| Discovery
|
||||
|-
|
||||
| T1078.004
|
||||
| Cloud Accounts
|
||||
| Defense Evasion, Initial Access, Persistence, Privilege Escalation
|
||||
@@ -5132,7 +5302,7 @@ Monitor your cloud infrastructure provisioning activities for behaviors originat
|
||||
===Suspicious cloud user activities===
|
||||
Detect and investigate suspicious activities by users and roles in your cloud environments.
|
||||
|
||||
* '''Product''': Splunk Security Analytics for AWS, Splunk Security Analytics for AWS, Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Product''': Splunk Security Analytics for AWS, Splunk Enterprise, Splunk Enterprise Security, Splunk Cloud
|
||||
* '''Datamodel''': Change
|
||||
* '''ATT&CK''': [https://attack.mitre.org/techniques/T1078.004/ T1078.004], [https://attack.mitre.org/techniques/T1078/ T1078]
|
||||
* '''Last Updated''': 2020-09-04
|
||||
@@ -6827,7 +6997,7 @@ Reduce the risk of CVE-2018-11409, an information disclosure vulnerability withi
|
||||
''
|
||||
#############
|
||||
# Automatically generated by doc_gen.py in https://github.com/splunk/security_content
|
||||
# On Date: 2021-04-02 17:10:21.639044 UTC
|
||||
# On Date: 2021-04-15 19:33:38.229931 UTC
|
||||
# Author: Splunk Security Research
|
||||
# Contact: research@splunk.com
|
||||
#############
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Splunk ES Content Update
|
||||
|
||||
This subscription service delivers pre-packaged Security Content for use with Splunk Enterprise Security. Subscribers get regular updates to help security practitioners more quickly address ongoing and time-sensitive customer problems and threats.
|
||||
|
||||
Requires Splunk Enterprise Security version 4.5 or greater.
|
||||
|
||||
For more information please visit the [Splunk ES Content Update user documentation](https://docs.splunk.com/Documentation/ESSOC).
|
||||
@@ -0,0 +1,15 @@
|
||||
The Analytic Story Details dashboard renders all the details of the content related to a specific analytic story which
|
||||
can be chose via the drop down
|
||||
|
||||
Each analytic story has attributes associated with it and the following:
|
||||
______________________________________________________________________
|
||||
|
||||
|
||||
Analytic Story: name of the analytic story
|
||||
Description ; description of the analytic story
|
||||
Search Name : The name of the searches belonging to the chosen analytic story
|
||||
Search : The search query which looks for an attack pattern corresponding to the analytic story
|
||||
Search Description: The description of the search query
|
||||
Asset Type: The analytic story specifies what asset in the infrastructure may be compromised
|
||||
Category: The category that the search belongs to (malware, vulnerabilities, best practices, abuse)
|
||||
Kill Chain Phase: The kill chain phase of the attack that the search is after.
|
||||
@@ -0,0 +1,24 @@
|
||||
The ES_SOC Summary Dashboard provides you a summarized view of the analytic story contents of the ES-SOC app.
|
||||
The dashboard has the following panels gives you following details
|
||||
|
||||
1) Analytic story Summary
|
||||
- Total Analytic Stories : The total number of Analytic stories in the ES-SOC application
|
||||
- Total Searches: The total number of searches in ES-SOC
|
||||
- Searches added last week: Number of searches added to ES-SOC in the last week.
|
||||
|
||||
2) Analytic story Category: This dashboard panel summarizes the categories of the searches that the ES-SOC app contains. The categories of the analytic stories are as follow
|
||||
-Malware: These searches detect specific malware behavior for a particular phase of the attack kill chain. E.g. a malware’s delivery method via email or a malware’s installation behavior via registry key changes
|
||||
-Vulnerability: These searches detect behavior or a signature of a vulnerable software in use. These searches are not designed to replace vulnerability management or scanning systems. The purpose of these searches is to discover a vulnerability through side effects or behaviors.
|
||||
-Abuse: Some actions can be deemed malicious because they are unexpected, violate corporate policy or are significantly different than the actions of other users. E.g. A USB disk that is seen on multiple systems or a user that uploads excessive files to a cloud service or a database query that dumps an entire table
|
||||
-Best Practices: Searches that correspond to specific guidelines from organizations like SANS or OWASP
|
||||
|
||||
3) Kill Chain phases: Every analytic story has one or more searches which look for a certain kind of attack pattern/behavior. These searches have an attribute which essentially tells you what Kill chain phase does the search correspond to.
|
||||
The numbers on the dashboard represents the number of searches correponding to each kill chain phase
|
||||
|
||||
4) Analytic story table: This table gives the user a comprehensive view of some of the details of the analytic story. Some of the listed attributes are:
|
||||
- Analytic Story : The name of the analytic story
|
||||
- Description: The description of the analyttic story
|
||||
- Search names: The name of the searches in each analytic story
|
||||
- Datamodels: The name of the datamodel that the search is querying against.
|
||||
- Technology Examples: This field represent some examples related to the technologies required to populate the datamodels(Nessues, Cisco Firewall,etc)
|
||||
- Kill chain phase: The name of the kill chain phase that the search belongs to
|
||||
@@ -0,0 +1,51 @@
|
||||
######################
|
||||
ESSOC Usage Dashboard#
|
||||
######################
|
||||
|
||||
The ESSOC Usage dashboard is designed to provide high-level insight into the usage of the ES-SOC app. It is suitable for display when providing feedback to the Splunk team or for identifying how the ES-SOC app is being used. This dashboard has two time selectors that work independently - the top time selector determines the search time range for all the single-value. And the lower time selector, determines the time range for the usage table.
|
||||
|
||||
IMPORTANT: The user loading this dashboard must have permission to search the _audit index
|
||||
|
||||
##################
|
||||
#Dashboard panels#
|
||||
##################
|
||||
|
||||
Searches Ran
|
||||
|
||||
The total number of searches in ES-SOC that were executed. This number includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax
|
||||
|
||||
Unique Searches
|
||||
|
||||
The unique/distinct searches executed on the deployment. This is equivalent to the distinct count of searches run in the ES-SOC app.
|
||||
|
||||
Most Run
|
||||
|
||||
The total number of searches in ES-SOC that were executed. This number includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Ad hoc Searches
|
||||
|
||||
The total number of searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Scheduled
|
||||
|
||||
The total number of ESSOC searches run that were scheduled.
|
||||
|
||||
Most Active User
|
||||
|
||||
The user who executed the highest number/count of searches. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Search Run Time (seconds)
|
||||
|
||||
Total run time of all searches executed in seconds. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Average Run Time (seconds)
|
||||
|
||||
Average run time of all searches executed in seconds. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Max Run Time (seconds)
|
||||
|
||||
The run time of the longest running search. This calculation includes scheduled searches and ad hoc searches run from the search bar using the '| savedsearch <ESSOC search_name> ‘ syntax.
|
||||
|
||||
Search summary
|
||||
|
||||
This table provides details on each search that was executed in the ESSOC app.
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"info": {
|
||||
"title": "ES Content Updates",
|
||||
"id": {
|
||||
"group": null,
|
||||
"name": "DA-ESS-ContentUpdate",
|
||||
"version": "3.19.0"
|
||||
},
|
||||
"author": [
|
||||
{
|
||||
"name": "Splunk Security Research Team",
|
||||
"email": "research@splunk.com",
|
||||
"company": "Splunk"
|
||||
}
|
||||
],
|
||||
"releaseDate": null,
|
||||
"description": "Explore the Analytic Stories included with ES Content Updates.",
|
||||
"classification": {
|
||||
"intendedAudience": null,
|
||||
"categories": [],
|
||||
"developmentStatus": null
|
||||
},
|
||||
"commonInformationModels": null,
|
||||
"license": {
|
||||
"name": null,
|
||||
"text": null,
|
||||
"uri": null
|
||||
},
|
||||
"privacyPolicy": {
|
||||
"name": null,
|
||||
"text": null,
|
||||
"uri": null
|
||||
},
|
||||
"releaseNotes": {
|
||||
"name": null,
|
||||
"text": "./README.md",
|
||||
"uri": null
|
||||
}
|
||||
},
|
||||
"dependencies": null,
|
||||
"tasks": null,
|
||||
"inputGroups": null,
|
||||
"incompatibleApps": null,
|
||||
"platformRequirements": null
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,187 @@
|
||||
/* .rTable {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rTableRow {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.rTableHeading {
|
||||
display: table-header-group;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.rTableCell, .rTableHead {
|
||||
display: table-cell;
|
||||
padding: 3px 10px;
|
||||
//border: 1px solid #999999;
|
||||
}
|
||||
|
||||
.rTableLeftCell {
|
||||
display: table-cell;
|
||||
padding: 3px 10px;
|
||||
//border: 1px solid #999999;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.rTableHeading {
|
||||
display: table-header-group;
|
||||
background-color: #ddd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rTableFoot {
|
||||
display: table-footer-group;
|
||||
font-weight: bold;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.rTableBody {
|
||||
display: table-row-group;
|
||||
} */
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 200;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.as_title_attr_bar {
|
||||
padding-left: 15%;
|
||||
background-color: #eee;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.as_title_attr {
|
||||
float: left;
|
||||
margin-right: 5%;
|
||||
padding-right: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.as_search_accordion {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.as_story_details {
|
||||
display: flex;
|
||||
max-height: 500px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.as_story_details_left_col {
|
||||
float: right;
|
||||
width: 34%;
|
||||
display: block;
|
||||
overflow: scroll;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.as_story_details_right_col {
|
||||
float: left;
|
||||
width: 60%;
|
||||
overflow: scroll;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.as_left_attr{
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.as_story_detail_left_attr_label {
|
||||
float: left;
|
||||
width: 30%;
|
||||
padding-top: 2%;
|
||||
}
|
||||
|
||||
.as_story_detail_left_attr {
|
||||
margin-top: 5px;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.value_label {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
background-color: #eee;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.as_story_detail_right_attr_label {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.search_content {
|
||||
display: flex;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.search_left_panel {
|
||||
float: left;
|
||||
width: 70%;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.search_right_panel {
|
||||
float: left;
|
||||
width: 25%;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.search_left_attr {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.search_right_attr {
|
||||
margin: 10px;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.search_string{
|
||||
padding: 10px;
|
||||
background-color: #ddd;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.data_model_tag {
|
||||
background-color: #11a88b;
|
||||
}
|
||||
|
||||
.kill_chain_tag {
|
||||
background-color: #ed8440;
|
||||
color: #303841;
|
||||
}
|
||||
|
||||
.attack_tag {
|
||||
background-color: #3863a0;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.heading-story {
|
||||
width: 80%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.run_story_btn {
|
||||
float: right;
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
require([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'splunkjs/mvc',
|
||||
'splunkjs/mvc/searchmanager',
|
||||
'splunkjs/mvc/searchbarview',
|
||||
'splunkjs/mvc/tableview',
|
||||
'splunk.util',
|
||||
'../app/DA-ESS-ContentUpdate/js/lib/showdown.min',
|
||||
'../app/DA-ESS-ContentUpdate/js/lib/jquery-ui/jquery-ui',
|
||||
'css!../app/DA-ESS-ContentUpdate/js/lib/jquery-ui/jquery-ui.css',
|
||||
'css!../app/DA-ESS-ContentUpdate/analytic_story_details.css',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function(_, $, mvc, SearchManager, SearchBarView, TableView, splunkUtil, showdown) {
|
||||
|
||||
let tokenModel = mvc.Components.get("default");
|
||||
let renderedComponents = [];
|
||||
|
||||
let templ = `
|
||||
<div class="as_title_attr_bar">
|
||||
<div class="as_title_attr">
|
||||
<strong>Category: </strong><span id="as_label_category"></span>
|
||||
</div>
|
||||
<div class="as_title_attr">
|
||||
<strong>Version: </strong><span id="as_label_version"></span>
|
||||
</div>
|
||||
<div class="as_title_attr">
|
||||
<strong>Created: </strong><span id="as_label_created"></span>
|
||||
</div>
|
||||
<div class="as_title_attr">
|
||||
<strong>Modified: </strong><span id="as_label_modified"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="headline_story">
|
||||
<div class="heading-story">
|
||||
<h1 id="story_heading"></h1>
|
||||
</div>
|
||||
<div class="run_story_btn">
|
||||
<button class="btn btn-primary run-story">Run Analytics</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_story_details">
|
||||
<div class="as_story_details_right_col">
|
||||
<div class="as_story_detail_right_attr_label">
|
||||
<strong>Description: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_right_attr_label">
|
||||
<span id="description"></span>
|
||||
</div>
|
||||
<div class="as_story_detail_right_attr_label">
|
||||
<strong>Narrative: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_right_attr_label narrative_value">
|
||||
<span id="narrative"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_story_details_left_col">
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>ATT&CK: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="mitre_attack">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>Kill Chain Phases: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr kill_chain_phases" id="kill_chain_phases">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>CIS Controls: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="cis_20">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>Data Model: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="data_model">
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_left_attr">
|
||||
<div class="as_story_detail_left_attr_label">
|
||||
<strong>References: </strong>
|
||||
</div>
|
||||
<div class="as_story_detail_left_attr" id="references">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="as_search_details">
|
||||
<h2>
|
||||
Analytic Story Searches
|
||||
</h2>
|
||||
<div id="accordion">
|
||||
<h3>Detection</h3>
|
||||
<div>
|
||||
<div id="search_detection">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Investigative</h3>
|
||||
<div>
|
||||
<div id="search_investigative">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Support</h3>
|
||||
<div>
|
||||
<div id="search_support">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#analytic_story_details').html(_.template(templ));
|
||||
|
||||
if (tokenModel.get('analytic_story_name')) {
|
||||
fetchAnalyticStoryDetails(tokenModel.get('analytic_story_name'));
|
||||
}
|
||||
|
||||
tokenModel.on("change:analytic_story_name", function(model, value, options) {
|
||||
fetchAnalyticStoryDetails(value);
|
||||
});
|
||||
|
||||
function fetchAnalyticStoryDetails(asName) {
|
||||
let epoch = (new Date).getTime();
|
||||
let searchGetAnalyticStoryData = new SearchManager({
|
||||
id: epoch,
|
||||
earliest_time: "-1h@h",
|
||||
latest_time: "now",
|
||||
cache: false,
|
||||
search: "| rest /services/configs/conf-analytic_stories splunk_server=local count=0 | search title=\"" + asName + "\" | spath input=reference path={} output=ref | spath input=data_models path={} output=dm | table title, category, description, version, mappings, creation_date, modification_date, dm, narrative, ref"
|
||||
});
|
||||
|
||||
$('.run-story').unbind('click');
|
||||
|
||||
$('.run-story').on('click', function(evt) {
|
||||
window.open('/en-US/app/Splunk_ASX/execute?form.mode=now&form.cron=*%2F15%20*%20*%20*%20*&form.earliest_time=-15m&form.latest_time=now&form.time.earliest=-24h%40h&form.time.latest=now&form.story=' + asName);
|
||||
});
|
||||
|
||||
let asSearch = splunkjs.mvc.Components.getInstance(epoch);
|
||||
let asResults = asSearch.data("results", {
|
||||
count: 0
|
||||
});
|
||||
asResults.on("data", function() {
|
||||
let as_attributes = {};
|
||||
let fields = asResults.data().fields;
|
||||
let rows = asResults.data().rows;
|
||||
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
as_attributes[fields[i]] = rows[0][i];
|
||||
}
|
||||
renderStoryAttributes(as_attributes);
|
||||
});
|
||||
|
||||
var searchGetSearchesData = new SearchManager({
|
||||
id: "s" + epoch,
|
||||
earliest_time: "-1h@h",
|
||||
latest_time: "now",
|
||||
cache: false,
|
||||
search: "| rest /services/saved/searches splunk_server=local count=0 | spath input=action.escu.analytic_story path={} output=uc | search uc = \"" + asName + "\" | spath input=action.escu.data_models path={} output=dm | table action.escu.full_search_name, search, description, action.escu.search_type, action.escu.how_to_implement, action.escu.eli5, action.escu.version, action.escu.mappings, dm, tex, action.escu.asset_at_risk, action.escu.confidence, action.escu.known_false_positives, updated, action.escu.modification_date, action.escu.creation_date "
|
||||
|
||||
});
|
||||
var searchesSearch = splunkjs.mvc.Components.getInstance("s" + epoch);
|
||||
var searchesResults = searchesSearch.data("results", {
|
||||
count: 0
|
||||
});
|
||||
|
||||
|
||||
searchesResults.on("data", function() {
|
||||
let asSearchAttr = [];
|
||||
var fields = searchesResults.data().fields;
|
||||
var rows = searchesResults.data().rows;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let searchObj = {};
|
||||
for (let j = 0; j < fields.length; j++) {
|
||||
searchObj[fields[j]] = rows[i][j];
|
||||
}
|
||||
asSearchAttr.push(searchObj);
|
||||
}
|
||||
renderSearches(asSearchAttr);
|
||||
});
|
||||
}
|
||||
|
||||
function renderStoryAttributes(asAttributes) {
|
||||
let converter = new showdown.Converter();
|
||||
let mappings = JSON.parse(asAttributes.mappings);
|
||||
$('#as_label_category').html(asAttributes.category);
|
||||
$('#as_label_version').html(asAttributes.version);
|
||||
$('#as_label_created').html(asAttributes.creation_date);
|
||||
$('#as_label_modified').html(asAttributes.modification_date);
|
||||
$('#story_heading').html(asAttributes.title);
|
||||
$('#attack').html(mappings.mitre_attack);
|
||||
$('#narrative').html(converter.makeHtml(asAttributes.narrative));
|
||||
$('#description').html(converter.makeHtml(asAttributes.description));
|
||||
$('#mitre_attack').html(getValueLabels(mappings.mitre_attack, 'attack_tag'));
|
||||
$('#data_model').html(getValueLabels(asAttributes.dm, 'data_model_tag'));
|
||||
$('#kill_chain_phases').html(getValueLabels(mappings.kill_chain_phases, 'kill_chain_tag'));
|
||||
$('#cis_20').html(getValueLabels(mappings.cis20));
|
||||
$('#references').html(getReferenceURLS(asAttributes.ref));
|
||||
}
|
||||
|
||||
function getReferenceURLS(refs) {
|
||||
if (refs === null) {
|
||||
return " ";
|
||||
} else {
|
||||
let refsResult = ``;
|
||||
if (Array.isArray(refs)) {
|
||||
refs.map(ref => {
|
||||
refsResult = refsResult + `<a href="${ ref }">${ ref }</a><br />`;
|
||||
});
|
||||
} else {
|
||||
refsResult = refsResult + `<a href="${ refs }">${ refs }</a><br />`
|
||||
}
|
||||
|
||||
return refsResult;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearches(asSearches) {
|
||||
clearSearchView();
|
||||
let i = 0;
|
||||
let converter = new showdown.Converter();
|
||||
asSearches.forEach(search => {
|
||||
i++;
|
||||
let epoch = (new Date).getTime();
|
||||
let searchID = `#search${ i }`;
|
||||
let resultID = `#result${ i }`;
|
||||
let searchSelector = `search${ i }`;
|
||||
let controlID = `as_search${ i }`
|
||||
let resultsControlID = `as_results_search${ i }`;
|
||||
let btnID = `btn_es_${i}`;
|
||||
|
||||
let searchPanel = `
|
||||
<h3>${ search['action.escu.full_search_name'] }</h3>
|
||||
<div class="search_content" id="${searchSelector}-content">
|
||||
<div class="search_left_panel">
|
||||
<button class="configure_in_es btn btn-primary" id="${ btnID }" data-search-type="${search['action.escu.search_type']}" data-search-name="${ search['action.escu.full_search_name'] }">Configure</button>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>Description</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['description']) }
|
||||
</div>
|
||||
</div>
|
||||
<div id="${searchSelector}-eli5">
|
||||
</div>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value ${ controlID }">
|
||||
</div>
|
||||
<div class="search_left_attr_value ${ resultsControlID }">
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>How to Implement</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.how_to_implement']) }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_left_attr">
|
||||
<div class="search_left_attr_label">
|
||||
<strong>Known False Positives</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.known_false_positives']) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_panel">
|
||||
<div class="search_right_attr data_model_srch_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Data Models</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(search['dm'], 'data_model_tag') }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (search['action.escu.search_type'] === "support") {
|
||||
//Process Support Search Accordion
|
||||
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_support').append(searchPanel);
|
||||
|
||||
// Adding extra params to support search
|
||||
let supportLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
$(`#${searchSelector}-eli5`).append(supportLeftAttr);
|
||||
|
||||
} else if (search['action.escu.search_type'] === "detection") {
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_detection').append(searchPanel);
|
||||
// Adding extra params to detection search
|
||||
let detectionAttrTop = `
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>ATT&CK</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(mappings.mitre_attack, 'attack_tag') }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Kill Chain Phases</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(mappings.kill_chain_phases, 'kill_chain_tag') }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>CIS Controls</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ getValueLabels(mappings.cis20) }
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let detectionAttrBottom = `
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Asset at Risk</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.asset_at_risk'] }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Confidence</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.confidence'] }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Creation Date</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.creation_date'] }
|
||||
</div>
|
||||
</div>
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Modification Date</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['action.escu.modification_date'] }
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
let detectionLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
$(detectionAttrTop).insertBefore($(`#${searchSelector}-content`).find('.data_model_srch_attr'));
|
||||
$(`#${searchSelector}-content`).find('.search_right_panel').append(detectionAttrBottom);
|
||||
$(`#${searchSelector}-eli5`).append(detectionLeftAttr);
|
||||
} else if (search['action.escu.search_type'] === "contextual") {
|
||||
//Process contextual Search Accordion
|
||||
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_contextual').append(searchPanel);
|
||||
|
||||
|
||||
// Adding extra params to contextual search
|
||||
let contextualLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
$(`#${searchSelector}-eli5`).append(contextualLeftAttr);
|
||||
|
||||
} else if (search['action.escu.search_type'] === "investigative") {
|
||||
//Process Investigative Search Accordion
|
||||
let mappings = JSON.parse(search['action.escu.mappings']);
|
||||
$('#search_investigative').append(searchPanel);
|
||||
|
||||
// Adding extra params to investigative search
|
||||
let investigativeLeftAttr = `<div class="search_left_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Explain It Like I'm 5</strong>
|
||||
</div>
|
||||
<div class="search_left_attr_value">
|
||||
${ converter.makeHtml(search['action.escu.eli5']) }
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
$(`#${searchSelector}-eli5`).append(investigativeLeftAttr);
|
||||
}
|
||||
|
||||
/*
|
||||
let updatedAttr = `
|
||||
<div class="search_right_attr">
|
||||
<div class="search_right_attr_label">
|
||||
<strong>Last Updated</strong>
|
||||
</div>
|
||||
<div class="search_right_attr_value">
|
||||
${ search['updated'] }
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$(`#${searchSelector}-content`).find('.search_right_panel').append(updatedAttr);
|
||||
*/
|
||||
|
||||
$(`#${ btnID }`).on('click', (evt) => {
|
||||
console.log($(evt.target).data("searchType"));
|
||||
if ($(evt.target).data("searchType") === "detection") {
|
||||
splunkUtil.redirect_to('app/SplunkEnterpriseSecuritySuite/correlation_search_edit', {
|
||||
search: `${$(evt.target).data("searchName")}`
|
||||
}, window.open(), true);
|
||||
} else {
|
||||
splunkUtil.redirect_to(`manager/DA-ESS-ContentUpdate/saved/searches`, {
|
||||
search: `${$(evt.target).data("searchName")}`
|
||||
}, window.open(), true);
|
||||
}
|
||||
})
|
||||
|
||||
let searchManagerID = search['action.escu.full_search_name'].split(' ').join('');
|
||||
|
||||
let searchManager = new SearchManager({
|
||||
id: searchManagerID,
|
||||
earliest_time: "-24h@h",
|
||||
latest_time: "now",
|
||||
status_buckets: 300,
|
||||
required_field_list: "*",
|
||||
preview: true,
|
||||
cache: true,
|
||||
autostart: false, // Prevent the search from running automatically
|
||||
search: search['search'],
|
||||
});
|
||||
|
||||
let searchBar = new SearchBarView({
|
||||
id: searchID,
|
||||
managerId: searchManagerID,
|
||||
timerange: true,
|
||||
el: $('.' + controlID),
|
||||
value: search['search'],
|
||||
timerange_preset: "Last 24 hours"
|
||||
}).render();
|
||||
|
||||
let tableviewer = new TableView({
|
||||
id: resultsControlID,
|
||||
managerid: searchManagerID,
|
||||
pageSize: 5,
|
||||
el: $("." + resultsControlID)
|
||||
}).render();
|
||||
|
||||
searchBar.on("change", function() {
|
||||
searchManager.settings.unset("search");
|
||||
|
||||
// Update the search query
|
||||
searchManager.settings.set("search", searchBar.val());
|
||||
|
||||
// Run the search (because autostart=false)
|
||||
searchManager.startSearch();
|
||||
});
|
||||
|
||||
searchBar.timerange.on("change", function() {
|
||||
// Update the time range of the search
|
||||
searchManager.search.set(searchBar.timerange.val());
|
||||
|
||||
// Run the search (because autostart=false)
|
||||
searchManager.startSearch();
|
||||
})
|
||||
|
||||
|
||||
renderedComponents.push(searchID, searchManagerID, resultsControlID);
|
||||
|
||||
});
|
||||
|
||||
$('#accordion').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_support').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_detection').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_contextual').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
$('#search_investigative').accordion({
|
||||
heightStyle: "content"
|
||||
});
|
||||
}
|
||||
|
||||
function clearSearchView() {
|
||||
if ($('#accordion').hasClass('ui-accordion')) {
|
||||
$('#accordion').accordion('destroy');
|
||||
}
|
||||
|
||||
if ($('#search_support').hasClass('ui-accordion')) {
|
||||
$('#search_support').accordion('destroy');
|
||||
$('#search_support').empty();
|
||||
}
|
||||
|
||||
if ($('#search_detection').hasClass('ui-accordion')) {
|
||||
$('#search_detection').accordion('destroy');
|
||||
$('#search_detection').empty();
|
||||
}
|
||||
|
||||
if ($('#search_contextual').hasClass('ui-accordion')) {
|
||||
$('#search_contextual').accordion('destroy');
|
||||
$('#search_contextual').empty();
|
||||
}
|
||||
|
||||
if ($('#search_investigative').hasClass('ui-accordion')) {
|
||||
$('#search_investigative').accordion('destroy');
|
||||
$('#search_investigative').empty();
|
||||
}
|
||||
|
||||
$('.configure_in_es').unbind("click");
|
||||
|
||||
let len = renderedComponents.length;
|
||||
while (len--) {
|
||||
let id = renderedComponents.pop();
|
||||
mvc.Components.getInstance(id).dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function getValueLabels(values, className) {
|
||||
let cls = "";
|
||||
if (className !== undefined || className) {
|
||||
cls = className;
|
||||
}
|
||||
let valueArray = [];
|
||||
if (values) {
|
||||
if (typeof values === "string") {
|
||||
valueArray.push(values)
|
||||
} else {
|
||||
valueArray = values;
|
||||
}
|
||||
}
|
||||
let htmlTmpl = "";
|
||||
valueArray.forEach(val => {
|
||||
htmlTmpl += `<div class="value_label ${ cls }">${ val }</div> `
|
||||
});
|
||||
|
||||
return htmlTmpl;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
.btn-pill {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.killchain-phases {
|
||||
width: 100%;
|
||||
margin-left: 6%;
|
||||
}
|
||||
|
||||
.killchain_card {
|
||||
width: 14%;
|
||||
float:left;
|
||||
}
|
||||
|
||||
.killchain {
|
||||
height: 55px;
|
||||
line-height: 55px;
|
||||
-webkit-clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%);
|
||||
clip-path: polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%);
|
||||
}
|
||||
|
||||
.killchain-text {
|
||||
font-size: 300%;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
|
||||
.killchain-label {
|
||||
margin-top: 7px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-left: 10%;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.killchain-text-one-line {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-family: system-ui;
|
||||
white-space: wrap;
|
||||
width: 100%;
|
||||
padding-top: 7%;
|
||||
padding-left: 26%;
|
||||
}
|
||||
|
||||
.killchain-text-second {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-family: system-ui;
|
||||
margin-top: -10px;
|
||||
padding-left: 26%;
|
||||
}
|
||||
|
||||
.killchain-phases {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notfirst {
|
||||
margin-left: -2%;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
require([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'splunkjs/mvc',
|
||||
'splunkjs/mvc/searchmanager',
|
||||
'../app/DA-ESS-ContentUpdate/js/lib/tabs',
|
||||
'css!../app/DA-ESS-ContentUpdate/js/lib/tabs.css',
|
||||
'css!../app/DA-ESS-ContentUpdate/escu_summary.css',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function(_, $, mvc, SearchManager) {
|
||||
$('.es-soc-analytic-story-stats').html(_.template('<%- _("Analytic Story Summary").t() %>'));
|
||||
$('.es-soc-search-stats').html(_.template('<%- _("Search Summary").t() %>'));
|
||||
|
||||
const tokenModel = mvc.Components.get('default');
|
||||
const submittedTokens = mvc.Components.get('submitted');
|
||||
|
||||
$.ajax({
|
||||
url: Splunk.util.make_url('/splunkd/__raw/servicesNS/nobody/DA-ESS-ContentUpdate/apps/local'),
|
||||
type: 'GET',
|
||||
async: true,
|
||||
data: {
|
||||
output_mode: 'json',
|
||||
count: -1,
|
||||
},
|
||||
}).done(result => {
|
||||
if (result.entry) {
|
||||
const foundEss = result.entry.find(app => app.name === 'SplunkEnterpriseSecuritySuite');
|
||||
if (foundEss.content.version === "5.2.0") {
|
||||
submittedTokens.set('explore-use-case-es-show', 'true');
|
||||
const use_case_library_link = Splunk.util.make_url('app/SplunkEnterpriseSecuritySuite/ess_use_case_library');
|
||||
const template = `<div class="alert alert-info"><i class="icon-alert" />
|
||||
${ _('Εxplore ESCU content updates directly from the Use Case Library within ES. To explore it, click').t() }
|
||||
<a href="<%- use_case_library_link %>"> ${ _('here').t() }</a>.
|
||||
</div>`;
|
||||
$('#explore-use-case-es-info').html(_.template(template, { use_case_library_link: use_case_library_link }));
|
||||
}
|
||||
}
|
||||
}).fail(err => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
// searchQuery -
|
||||
let kcpSearch = new SearchManager({
|
||||
id: "kcpSearch",
|
||||
preview: true,
|
||||
cache: true,
|
||||
status_buckets: 300,
|
||||
earliest_time: '-24h@h',
|
||||
latest_time: 'now',
|
||||
search: '| rest /services/configs/conf-analytic_stories splunk_server=local count=0 | spath input=mappings path=kill_chain_phases{} output=kcp | stats count by kcp',
|
||||
});
|
||||
|
||||
let results = kcpSearch.data("preview");
|
||||
|
||||
results.on("data", function() {
|
||||
results.data().rows.forEach(row => {
|
||||
let killchainID = '#' + row[0].toLowerCase().replace(/ /g,'');
|
||||
$(killchainID).html(row[1]);
|
||||
});
|
||||
});
|
||||
|
||||
$('#analytic_filter_clear').on('click', function() {
|
||||
tokenModel.set('form.as_cis', '*');
|
||||
tokenModel.set('form.as_category', '*');
|
||||
tokenModel.set('form.as_kill_chain_phase', '*');
|
||||
tokenModel.set('form.as_data_models', '*');
|
||||
});
|
||||
|
||||
$('#search_filter_clear').on('click', function() {
|
||||
tokenModel.set('form.cis', '*');
|
||||
tokenModel.set('form.searchtype', '*');
|
||||
tokenModel.set('form.kill_chain_phase', '*');
|
||||
tokenModel.set('form.data_models', '*');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
require([
|
||||
'underscore',
|
||||
'jquery',
|
||||
'splunkjs/mvc',
|
||||
'splunkjs/mvc/simplexml/ready!'
|
||||
], function(_, $, mvc, TableView) {
|
||||
|
||||
var defaultTokenSpace = mvc.Components.getInstance('default');
|
||||
|
||||
// This will take every textarea that has a data-token attribute and will make the given token with the value of the textarea
|
||||
$('textarea[data-token]').each(function (textarea) {
|
||||
$(this).on('input', function(input) {
|
||||
var token_to_set = $(this).data('token');
|
||||
defaultTokenSpace.set(token_to_set, $(this).val());
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 215 KiB |
@@ -0,0 +1,333 @@
|
||||
Authors ordered by first contribution
|
||||
A list of current team members is available at http://jqueryui.com/about
|
||||
|
||||
Paul Bakaus <paul.bakaus@gmail.com>
|
||||
Richard Worth <rdworth@gmail.com>
|
||||
Yehuda Katz <wycats@gmail.com>
|
||||
Sean Catchpole <sean@sunsean.com>
|
||||
John Resig <jeresig@gmail.com>
|
||||
Tane Piper <piper.tane@gmail.com>
|
||||
Dmitri Gaskin <dmitrig01@gmail.com>
|
||||
Klaus Hartl <klaus.hartl@gmail.com>
|
||||
Stefan Petre <stefan.petre@gmail.com>
|
||||
Gilles van den Hoven <gilles@webunity.nl>
|
||||
Micheil Bryan Smith <micheil@brandedcode.com>
|
||||
Jörn Zaefferer <joern.zaefferer@gmail.com>
|
||||
Marc Grabanski <m@marcgrabanski.com>
|
||||
Keith Wood <kbwood@iinet.com.au>
|
||||
Brandon Aaron <brandon.aaron@gmail.com>
|
||||
Scott González <scott.gonzalez@gmail.com>
|
||||
Eduardo Lundgren <eduardolundgren@gmail.com>
|
||||
Aaron Eisenberger <aaronchi@gmail.com>
|
||||
Joan Piedra <theneojp@gmail.com>
|
||||
Bruno Basto <b.basto@gmail.com>
|
||||
Remy Sharp <remy@leftlogic.com>
|
||||
Bohdan Ganicky <bohdan.ganicky@gmail.com>
|
||||
David Bolter <david.bolter@gmail.com>
|
||||
Chi Cheng <cloudream@gmail.com>
|
||||
Ca-Phun Ung <pazu2k@gmail.com>
|
||||
Ariel Flesler <aflesler@gmail.com>
|
||||
Maggie Wachs <maggie@filamentgroup.com>
|
||||
Scott Jehl <scottjehl@gmail.com>
|
||||
Todd Parker <todd@filamentgroup.com>
|
||||
Andrew Powell <andrew@shellscape.org>
|
||||
Brant Burnett <btburnett3@gmail.com>
|
||||
Douglas Neiner <doug@dougneiner.com>
|
||||
Paul Irish <paul.irish@gmail.com>
|
||||
Ralph Whitbeck <ralph.whitbeck@gmail.com>
|
||||
Thibault Duplessis <thibault.duplessis@gmail.com>
|
||||
Dominique Vincent <dominique.vincent@toitl.com>
|
||||
Jack Hsu <jack.hsu@gmail.com>
|
||||
Adam Sontag <ajpiano@ajpiano.com>
|
||||
Carl Fürstenberg <carl@excito.com>
|
||||
Kevin Dalman <development@allpro.net>
|
||||
Alberto Fernández Capel <afcapel@gmail.com>
|
||||
Jacek Jędrzejewski (http://jacek.jedrzejewski.name)
|
||||
Ting Kuei <ting@kuei.com>
|
||||
Samuel Cormier-Iijima <sam@chide.it>
|
||||
Jon Palmer <jonspalmer@gmail.com>
|
||||
Ben Hollis <bhollis@amazon.com>
|
||||
Justin MacCarthy <Justin@Rubystars.biz>
|
||||
Eyal Kobrigo <kobrigo@hotmail.com>
|
||||
Tiago Freire <tiago.freire@gmail.com>
|
||||
Diego Tres <diegotres@gmail.com>
|
||||
Holger Rüprich <holger@rueprich.de>
|
||||
Ziling Zhao <zilingzhao@gmail.com>
|
||||
Mike Alsup <malsup@gmail.com>
|
||||
Robson Braga Araujo <robsonbraga@gmail.com>
|
||||
Pierre-Henri Ausseil <ph.ausseil@gmail.com>
|
||||
Christopher McCulloh <cmcculloh@gmail.com>
|
||||
Andrew Newcomb <ext.github@preceptsoftware.co.uk>
|
||||
Lim Chee Aun <cheeaun@gmail.com>
|
||||
Jorge Barreiro <yortx.barry@gmail.com>
|
||||
Daniel Steigerwald <daniel@steigerwald.cz>
|
||||
John Firebaugh <john_firebaugh@bigfix.com>
|
||||
John Enters <github@darkdark.net>
|
||||
Andrey Kapitcyn <ru.m157y@gmail.com>
|
||||
Dmitry Petrov <dpetroff@gmail.com>
|
||||
Eric Hynds <eric@hynds.net>
|
||||
Chairat Sunthornwiphat <pipo@sixhead.com>
|
||||
Josh Varner <josh.varner@gmail.com>
|
||||
Stéphane Raimbault <stephane.raimbault@gmail.com>
|
||||
Jay Merrifield <fracmak@gmail.com>
|
||||
J. Ryan Stinnett <jryans@gmail.com>
|
||||
Peter Heiberg <peter@heiberg.se>
|
||||
Alex Dovenmuehle <adovenmuehle@gmail.com>
|
||||
Jamie Gegerson <git@jamiegegerson.com>
|
||||
Raymond Schwartz <skeetergraphics@gmail.com>
|
||||
Phillip Barnes <philbar@gmail.com>
|
||||
Kyle Wilkinson <kai@wikyd.org>
|
||||
Khaled AlHourani <me@khaledalhourani.com>
|
||||
Marian Rudzynski <mr@impaled.org>
|
||||
Jean-Francois Remy <jeff@melix.org>
|
||||
Doug Blood <dougblood@gmail.com>
|
||||
Filippo Cavallarin <filippo.cavallarin@codseq.it>
|
||||
Heiko Henning <heiko@thehennings.ch>
|
||||
Aliaksandr Rahalevich <saksmlz@gmail.com>
|
||||
Mario Visic <mario@mariovisic.com>
|
||||
Xavi Ramirez <xavi.rmz@gmail.com>
|
||||
Max Schnur <max.schnur@gmail.com>
|
||||
Saji Nediyanchath <saji89@gmail.com>
|
||||
Corey Frang <gnarf37@gmail.com>
|
||||
Aaron Peterson <aaronp123@yahoo.com>
|
||||
Ivan Peters <ivan@ivanpeters.com>
|
||||
Mohamed Cherif Bouchelaghem <cherifbouchelaghem@yahoo.fr>
|
||||
Marcos Sousa <falecomigo@marcossousa.com>
|
||||
Michael DellaNoce <mdellanoce@mailtrust.com>
|
||||
George Marshall <echosx@gmail.com>
|
||||
Tobias Brunner <tobias@strongswan.org>
|
||||
Martin Solli <msolli@gmail.com>
|
||||
David Petersen <public@petersendidit.com>
|
||||
Dan Heberden <danheberden@gmail.com>
|
||||
William Kevin Manire <williamkmanire@gmail.com>
|
||||
Gilmore Davidson <gilmoreorless@gmail.com>
|
||||
Michael Wu <michaelmwu@gmail.com>
|
||||
Adam Parod <mystic414@gmail.com>
|
||||
Guillaume Gautreau <guillaume+github@ghusse.com>
|
||||
Marcel Toele <EleotleCram@gmail.com>
|
||||
Dan Streetman <ddstreet@ieee.org>
|
||||
Matt Hoskins <matt@nipltd.com>
|
||||
Giovanni Giacobbi <giovanni@giacobbi.net>
|
||||
Kyle Florence <kyle.florence@gmail.com>
|
||||
Pavol Hluchý <lopo@losys.sk>
|
||||
Hans Hillen <hans.hillen@gmail.com>
|
||||
Mark Johnson <virgofx@live.com>
|
||||
Trey Hunner <treyhunner@gmail.com>
|
||||
Shane Whittet <whittet@gmail.com>
|
||||
Edward A Faulkner <ef@alum.mit.edu>
|
||||
Adam Baratz <adam@adambaratz.com>
|
||||
Kato Kazuyoshi <kato.kazuyoshi@gmail.com>
|
||||
Eike Send <eike.send@gmail.com>
|
||||
Kris Borchers <kris.borchers@gmail.com>
|
||||
Eddie Monge <eddie@eddiemonge.com>
|
||||
Israel Tsadok <itsadok@gmail.com>
|
||||
Carson McDonald <carson@ioncannon.net>
|
||||
Jason Davies <jason@jasondavies.com>
|
||||
Garrison Locke <gplocke@gmail.com>
|
||||
David Murdoch <david@davidmurdoch.com>
|
||||
Benjamin Scott Boyle <benjamins.boyle@gmail.com>
|
||||
Jesse Baird <jebaird@gmail.com>
|
||||
Jonathan Vingiano <jvingiano@gmail.com>
|
||||
Dylan Just <dev@ephox.com>
|
||||
Hiroshi Tomita <tomykaira@gmail.com>
|
||||
Glenn Goodrich <glenn.goodrich@gmail.com>
|
||||
Tarafder Ashek-E-Elahi <mail.ashek@gmail.com>
|
||||
Ryan Neufeld <ryan@neufeldmail.com>
|
||||
Marc Neuwirth <marc.neuwirth@gmail.com>
|
||||
Philip Graham <philip.robert.graham@gmail.com>
|
||||
Benjamin Sterling <benjamin.sterling@kenzomedia.com>
|
||||
Wesley Walser <waw325@gmail.com>
|
||||
Kouhei Sutou <kou@clear-code.com>
|
||||
Karl Kirch <karlkrch@gmail.com>
|
||||
Chris Kelly <ckdake@ckdake.com>
|
||||
Jason Oster <jay@kodewerx.org>
|
||||
Felix Nagel <info@felixnagel.com>
|
||||
Alexander Polomoshnov <alex.polomoshnov@gmail.com>
|
||||
David Leal <dgleal@gmail.com>
|
||||
Igor Milla <igor.fsp.milla@gmail.com>
|
||||
Dave Methvin <dave.methvin@gmail.com>
|
||||
Florian Gutmann <f.gutmann@chronimo.com>
|
||||
Marwan Al Jubeh <marwan.aljubeh@gmail.com>
|
||||
Milan Broum <midlis@googlemail.com>
|
||||
Sebastian Sauer <info@dynpages.de>
|
||||
Gaëtan Muller <m.gaetan89@gmail.com>
|
||||
Michel Weimerskirch <michel@weimerskirch.net>
|
||||
William Griffiths <william@ycymro.com>
|
||||
Stojce Slavkovski <stojce@gmail.com>
|
||||
David Soms <david.soms@gmail.com>
|
||||
David De Sloovere <david.desloovere@outlook.com>
|
||||
Michael P. Jung <michael.jung@terreon.de>
|
||||
Shannon Pekary <spekary@gmail.com>
|
||||
Dan Wellman <danwellman@hotmail.com>
|
||||
Matthew Edward Hutton <meh@corefiling.co.uk>
|
||||
James Khoury <james@jameskhoury.com>
|
||||
Rob Loach <robloach@gmail.com>
|
||||
Alberto Monteiro <betimbrasil@gmail.com>
|
||||
Alex Rhea <alex.rhea@gmail.com>
|
||||
Krzysztof Rosiński <rozwell69@gmail.com>
|
||||
Ryan Olton <oltonr@gmail.com>
|
||||
Genie <386@mail.com>
|
||||
Rick Waldron <waldron.rick@gmail.com>
|
||||
Ian Simpson <spoonlikesham@gmail.com>
|
||||
Lev Kitsis <spam4lev@gmail.com>
|
||||
TJ VanToll <tj.vantoll@gmail.com>
|
||||
Justin Domnitz <jdomnitz@gmail.com>
|
||||
Douglas Cerna <douglascerna@yahoo.com>
|
||||
Bert ter Heide <bertjh@hotmail.com>
|
||||
Jasvir Nagra <jasvir@gmail.com>
|
||||
Yuriy Khabarov <13real008@gmail.com>
|
||||
Harri Kilpiö <harri.kilpio@gmail.com>
|
||||
Lado Lomidze <lado.lomidze@gmail.com>
|
||||
Amir E. Aharoni <amir.aharoni@mail.huji.ac.il>
|
||||
Simon Sattes <simon.sattes@gmail.com>
|
||||
Jo Liss <joliss42@gmail.com>
|
||||
Guntupalli Karunakar <karunakarg@yahoo.com>
|
||||
Shahyar Ghobadpour <shahyar@gmail.com>
|
||||
Lukasz Lipinski <uzza17@gmail.com>
|
||||
Timo Tijhof <krinklemail@gmail.com>
|
||||
Jason Moon <jmoon@socialcast.com>
|
||||
Martin Frost <martinf55@hotmail.com>
|
||||
Eneko Illarramendi <eneko@illarra.com>
|
||||
EungJun Yi <semtlenori@gmail.com>
|
||||
Courtland Allen <courtlandallen@gmail.com>
|
||||
Viktar Varvanovich <non4eg@gmail.com>
|
||||
Danny Trunk <dtrunk90@gmail.com>
|
||||
Pavel Stetina <pavel.stetina@nangu.tv>
|
||||
Michael Stay <metaweta@gmail.com>
|
||||
Steven Roussey <sroussey@gmail.com>
|
||||
Michael Hollis <hollis21@gmail.com>
|
||||
Lee Rowlands <lee.rowlands@previousnext.com.au>
|
||||
Timmy Willison <timmywillisn@gmail.com>
|
||||
Karl Swedberg <kswedberg@gmail.com>
|
||||
Baoju Yuan <the_guy_1987@hotmail.com>
|
||||
Maciej Mroziński <maciej.k.mrozinski@gmail.com>
|
||||
Luis Dalmolin <luis.nh@gmail.com>
|
||||
Mark Aaron Shirley <maspwr@gmail.com>
|
||||
Martin Hoch <martin@fidion.de>
|
||||
Jiayi Yang <tr870829@gmail.com>
|
||||
Philipp Benjamin Köppchen <xgxtpbk@gws.ms>
|
||||
Sindre Sorhus <sindresorhus@gmail.com>
|
||||
Bernhard Sirlinger <bernhard.sirlinger@tele2.de>
|
||||
Jared A. Scheel <jared@jaredscheel.com>
|
||||
Rafael Xavier de Souza <rxaviers@gmail.com>
|
||||
John Chen <zhang.z.chen@intel.com>
|
||||
Robert Beuligmann <robertbeuligmann@gmail.com>
|
||||
Dale Kocian <dale.kocian@gmail.com>
|
||||
Mike Sherov <mike.sherov@gmail.com>
|
||||
Andrew Couch <andy@couchand.com>
|
||||
Marc-Andre Lafortune <github@marc-andre.ca>
|
||||
Nate Eagle <nate.eagle@teamaol.com>
|
||||
David Souther <davidsouther@gmail.com>
|
||||
Mathias Stenbom <mathias@stenbom.com>
|
||||
Sergey Kartashov <ebishkek@yandex.ru>
|
||||
Avinash R <nashpapa@gmail.com>
|
||||
Ethan Romba <ethanromba@gmail.com>
|
||||
Cory Gackenheimer <cory.gack@gmail.com>
|
||||
Juan Pablo Kaniefsky <jpkaniefsky@gmail.com>
|
||||
Roman Salnikov <bardt.dz@gmail.com>
|
||||
Anika Henke <anika@selfthinker.org>
|
||||
Samuel Bovée <samycookie2000@yahoo.fr>
|
||||
Fabrício Matté <ult_combo@hotmail.com>
|
||||
Viktor Kojouharov <vkojouharov@gmail.com>
|
||||
Pawel Maruszczyk (http://hrabstwo.net)
|
||||
Pavel Selitskas <p.selitskas@gmail.com>
|
||||
Bjørn Johansen <post@bjornjohansen.no>
|
||||
Matthieu Penant <thieum22@hotmail.com>
|
||||
Dominic Barnes <dominic@dbarnes.info>
|
||||
David Sullivan <david.sullivan@gmail.com>
|
||||
Thomas Jaggi <thomas@responsive.ch>
|
||||
Vahid Sohrabloo <vahid4134@gmail.com>
|
||||
Travis Carden <travis.carden@gmail.com>
|
||||
Bruno M. Custódio <bruno@brunomcustodio.com>
|
||||
Nathanael Silverman <nathanael.silverman@gmail.com>
|
||||
Christian Wenz <christian@wenz.org>
|
||||
Steve Urmston <steve@urm.st>
|
||||
Zaven Muradyan <megalivoithos@gmail.com>
|
||||
Woody Gilk <shadowhand@deviantart.com>
|
||||
Zbigniew Motyka <zbigniew.motyka@gmail.com>
|
||||
Suhail Alkowaileet <xsoh.k7@gmail.com>
|
||||
Toshi MARUYAMA <marutosijp2@yahoo.co.jp>
|
||||
David Hansen <hansede@gmail.com>
|
||||
Brian Grinstead <briangrinstead@gmail.com>
|
||||
Christian Klammer <christian314159@gmail.com>
|
||||
Steven Luscher <jquerycla@steveluscher.com>
|
||||
Gan Eng Chin <engchin.gan@gmail.com>
|
||||
Gabriel Schulhof <gabriel.schulhof@intel.com>
|
||||
Alexander Schmitz <arschmitz@gmail.com>
|
||||
Vilhjálmur Skúlason <vis@dmm.is>
|
||||
Siebrand Mazeland <siebrand@kitano.nl>
|
||||
Mohsen Ekhtiari <mohsenekhtiari@yahoo.com>
|
||||
Pere Orga <gotrunks@gmail.com>
|
||||
Jasper de Groot <mail@ugomobi.com>
|
||||
Stephane Deschamps <stephane.deschamps@gmail.com>
|
||||
Jyoti Deka <dekajp@gmail.com>
|
||||
Andrei Picus <office.nightcrawler@gmail.com>
|
||||
Ondrej Novy <novy@ondrej.org>
|
||||
Jacob McCutcheon <jacob.mccutcheon@gmail.com>
|
||||
Monika Piotrowicz <monika.piotrowicz@gmail.com>
|
||||
Imants Horsts <imants.horsts@inbox.lv>
|
||||
Eric Dahl <eric.c.dahl@gmail.com>
|
||||
Dave Stein <dave@behance.com>
|
||||
Dylan Barrell <dylan@barrell.com>
|
||||
Daniel DeGroff <djdegroff@gmail.com>
|
||||
Michael Wiencek <mwtuea@gmail.com>
|
||||
Thomas Meyer <meyertee@gmail.com>
|
||||
Ruslan Yakhyaev <ruslan@ruslan.io>
|
||||
Brian J. Dowling <bjd-dev@simplicity.net>
|
||||
Ben Higgins <ben@extrahop.com>
|
||||
Yermo Lamers <yml@yml.com>
|
||||
Patrick Stapleton <github@gdi2290.com>
|
||||
Trisha Crowley <trisha.crowley@gmail.com>
|
||||
Usman Akeju <akeju00+github@gmail.com>
|
||||
Rodrigo Menezes <rod333@gmail.com>
|
||||
Jacques Perrault <jacques_perrault@us.ibm.com>
|
||||
Frederik Elvhage <frederik.elvhage@googlemail.com>
|
||||
Will Holley <willholley@gmail.com>
|
||||
Uri Gilad <antishok@gmail.com>
|
||||
Richard Gibson <richard.gibson@gmail.com>
|
||||
Simen Bekkhus <sbekkhus91@gmail.com>
|
||||
Chen Eshchar <eshcharc@gmail.com>
|
||||
Bruno Pérel <brunoperel@gmail.com>
|
||||
Mohammed Alshehri <m@dralshehri.com>
|
||||
Lisa Seacat DeLuca <ldeluca@us.ibm.com>
|
||||
Anne-Gaelle Colom <coloma@westminster.ac.uk>
|
||||
Adam Foster <slimfoster@gmail.com>
|
||||
Luke Page <luke.a.page@gmail.com>
|
||||
Daniel Owens <daniel@matchstickmixup.com>
|
||||
Michael Orchard <morchard@scottlogic.co.uk>
|
||||
Marcus Warren <marcus@envoke.com>
|
||||
Nils Heuermann <nils@world-of-scripts.de>
|
||||
Marco Ziech <marco@ziech.net>
|
||||
Patricia Juarez <patrixd@gmail.com>
|
||||
Ben Mosher <me@benmosher.com>
|
||||
Ablay Keldibek <atomio.ak@gmail.com>
|
||||
Thomas Applencourt <thomas.applencourt@irsamc.ups-tlse.fr>
|
||||
Jiabao Wu <jiabao.foss@gmail.com>
|
||||
Eric Lee Carraway <github@ericcarraway.com>
|
||||
Victor Homyakov <vkhomyackov@gmail.com>
|
||||
Myeongjin Lee <aranet100@gmail.com>
|
||||
Liran Sharir <lsharir@gmail.com>
|
||||
Weston Ruter <weston@xwp.co>
|
||||
Mani Mishra <manimishra902@gmail.com>
|
||||
Hannah Methvin <hannahmethvin@gmail.com>
|
||||
Leonardo Balter <leonardo.balter@gmail.com>
|
||||
Benjamin Albert <benjamin_a5@yahoo.com>
|
||||
Michał Gołębiowski <m.goleb@gmail.com>
|
||||
Alyosha Pushak <alyosha.pushak@gmail.com>
|
||||
Fahad Ahmad <fahadahmad41@hotmail.com>
|
||||
Matt Brundage <github@mattbrundage.com>
|
||||
Francesc Baeta <francesc.baeta@gmail.com>
|
||||
Piotr Baran <piotros@wp.pl>
|
||||
Mukul Hase <mukulhase@gmail.com>
|
||||
Konstantin Dinev <kdinev@mail.bw.edu>
|
||||
Rand Scullard <rand@randscullard.com>
|
||||
Dan Strohl <dan@wjcg.net>
|
||||
Maksim Ryzhikov <rv.maksim@gmail.com>
|
||||
Amine HADDAD <haddad@allegorie.tv>
|
||||
Amanpreet Singh <apsdehal@gmail.com>
|
||||
Alexey Balchunas <bleshik@gmail.com>
|
||||
Peter Kehl <peter.kehl@gmail.com>
|
||||
Peter Dave Hello <hsu@peterdavehello.org>
|
||||
Johannes Schäfer <johnschaefer@gmx.de>
|
||||
Ville Skyttä <ville.skytta@iki.fi>
|
||||
Ryan Oriecuia <ryan.oriecuia@visioncritical.com>
|
||||
@@ -0,0 +1,43 @@
|
||||
Copyright jQuery Foundation and other contributors, https://jquery.org/
|
||||
|
||||
This software consists of voluntary contributions made by many
|
||||
individuals. For exact contribution history, see the revision history
|
||||
available at https://github.com/jquery/jquery-ui
|
||||
|
||||
The following license applies to all parts of this software except as
|
||||
documented below:
|
||||
|
||||
====
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
Copyright and related rights for sample code are waived via CC0. Sample
|
||||
code is defined as all source code contained within the demos directory.
|
||||
|
||||
CC0: http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
====
|
||||
|
||||
All files located in the node_modules and external directories are
|
||||
externally maintained libraries used by this software which have their
|
||||
own licenses; we recommend you read them, as their terms may differ from
|
||||
the terms above.
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,559 @@
|
||||
<!doctype html>
|
||||
<html lang="us">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>jQuery UI Example Page</title>
|
||||
<link href="jquery-ui.css" rel="stylesheet">
|
||||
<style>
|
||||
body{
|
||||
font-family: "Trebuchet MS", sans-serif;
|
||||
margin: 50px;
|
||||
}
|
||||
.demoHeaders {
|
||||
margin-top: 2em;
|
||||
}
|
||||
#dialog-link {
|
||||
padding: .4em 1em .4em 20px;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
}
|
||||
#dialog-link span.ui-icon {
|
||||
margin: 0 5px 0 0;
|
||||
position: absolute;
|
||||
left: .2em;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
#icons {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#icons li {
|
||||
margin: 2px;
|
||||
position: relative;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
list-style: none;
|
||||
}
|
||||
#icons span.ui-icon {
|
||||
float: left;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.fakewindowcontain .ui-widget-overlay {
|
||||
position: absolute;
|
||||
}
|
||||
select {
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Welcome to jQuery UI!</h1>
|
||||
|
||||
<div class="ui-widget">
|
||||
<p>This page demonstrates the widgets and theme you selected in Download Builder. Please make sure you are using them with a compatible jQuery version.</p>
|
||||
</div>
|
||||
|
||||
<h1>YOUR COMPONENTS:</h1>
|
||||
|
||||
|
||||
<!-- Accordion -->
|
||||
<h2 class="demoHeaders">Accordion</h2>
|
||||
<div id="accordion">
|
||||
<h3>First</h3>
|
||||
<div>Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet.</div>
|
||||
<h3>Second</h3>
|
||||
<div>Phasellus mattis tincidunt nibh.</div>
|
||||
<h3>Third</h3>
|
||||
<div>Nam dui erat, auctor a, dignissim quis.</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Autocomplete -->
|
||||
<h2 class="demoHeaders">Autocomplete</h2>
|
||||
<div>
|
||||
<input id="autocomplete" title="type "a"">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Button -->
|
||||
<h2 class="demoHeaders">Button</h2>
|
||||
<button id="button">A button element</button>
|
||||
<button id="button-icon">An icon-only button</button>
|
||||
|
||||
|
||||
|
||||
<!-- Checkboxradio -->
|
||||
<h2 class="demoHeaders">Checkboxradio</h2>
|
||||
<form style="margin-top: 1em;">
|
||||
<div id="radioset">
|
||||
<input type="radio" id="radio1" name="radio"><label for="radio1">Choice 1</label>
|
||||
<input type="radio" id="radio2" name="radio" checked="checked"><label for="radio2">Choice 2</label>
|
||||
<input type="radio" id="radio3" name="radio"><label for="radio3">Choice 3</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<!-- Controlgroup -->
|
||||
<h2 class="demoHeaders">Controlgroup</h2>
|
||||
<fieldset>
|
||||
<legend>Rental Car</legend>
|
||||
<div id="controlgroup">
|
||||
<select id="car-type">
|
||||
<option>Compact car</option>
|
||||
<option>Midsize car</option>
|
||||
<option>Full size car</option>
|
||||
<option>SUV</option>
|
||||
<option>Luxury</option>
|
||||
<option>Truck</option>
|
||||
<option>Van</option>
|
||||
</select>
|
||||
<label for="transmission-standard">Standard</label>
|
||||
<input type="radio" name="transmission" id="transmission-standard">
|
||||
<label for="transmission-automatic">Automatic</label>
|
||||
<input type="radio" name="transmission" id="transmission-automatic">
|
||||
<label for="insurance">Insurance</label>
|
||||
<input type="checkbox" name="insurance" id="insurance">
|
||||
<label for="horizontal-spinner" class="ui-controlgroup-label"># of cars</label>
|
||||
<input id="horizontal-spinner" class="ui-spinner-input">
|
||||
<button>Book Now!</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
|
||||
<!-- Tabs -->
|
||||
<h2 class="demoHeaders">Tabs</h2>
|
||||
<div id="tabs">
|
||||
<ul>
|
||||
<li><a href="#tabs-1">First</a></li>
|
||||
<li><a href="#tabs-2">Second</a></li>
|
||||
<li><a href="#tabs-3">Third</a></li>
|
||||
</ul>
|
||||
<div id="tabs-1">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</div>
|
||||
<div id="tabs-2">Phasellus mattis tincidunt nibh. Cras orci urna, blandit id, pretium vel, aliquet ornare, felis. Maecenas scelerisque sem non nisl. Fusce sed lorem in enim dictum bibendum.</div>
|
||||
<div id="tabs-3">Nam dui erat, auctor a, dignissim quis, sollicitudin eu, felis. Pellentesque nisi urna, interdum eget, sagittis et, consequat vestibulum, lacus. Mauris porttitor ullamcorper augue.</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h2 class="demoHeaders">Dialog</h2>
|
||||
<p>
|
||||
<button id="dialog-link" class="ui-button ui-corner-all ui-widget">
|
||||
<span class="ui-icon ui-icon-newwin"></span>Open Dialog
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<h2 class="demoHeaders">Overlay and Shadow Classes</h2>
|
||||
<div style="position: relative; width: 96%; height: 200px; padding:1% 2%; overflow:hidden;" class="fakewindowcontain">
|
||||
<p>Lorem ipsum dolor sit amet, Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. </p><p>Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. </p><p>Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. </p><p>Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. </p>
|
||||
|
||||
<!-- ui-dialog -->
|
||||
<div class="ui-widget-overlay ui-front"></div>
|
||||
<div style="position: absolute; width: 320px; left: 50px; top: 30px; padding: 1.2em" class="ui-widget ui-front ui-widget-content ui-corner-all ui-widget-shadow">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ui-dialog -->
|
||||
<div id="dialog" title="Dialog Title">
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h2 class="demoHeaders">Framework Icons (content color preview)</h2>
|
||||
<ul id="icons" class="ui-widget ui-helper-clearfix">
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-n"><span class="ui-icon ui-icon-caret-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-ne"><span class="ui-icon ui-icon-caret-1-ne"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-e"><span class="ui-icon ui-icon-caret-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-se"><span class="ui-icon ui-icon-caret-1-se"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-s"><span class="ui-icon ui-icon-caret-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-sw"><span class="ui-icon ui-icon-caret-1-sw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-w"><span class="ui-icon ui-icon-caret-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-nw"><span class="ui-icon ui-icon-caret-1-nw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-n-s"><span class="ui-icon ui-icon-caret-2-n-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-e-w"><span class="ui-icon ui-icon-caret-2-e-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-n"><span class="ui-icon ui-icon-triangle-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-ne"><span class="ui-icon ui-icon-triangle-1-ne"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-e"><span class="ui-icon ui-icon-triangle-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-se"><span class="ui-icon ui-icon-triangle-1-se"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-s"><span class="ui-icon ui-icon-triangle-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-sw"><span class="ui-icon ui-icon-triangle-1-sw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-w"><span class="ui-icon ui-icon-triangle-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-nw"><span class="ui-icon ui-icon-triangle-1-nw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-n-s"><span class="ui-icon ui-icon-triangle-2-n-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-e-w"><span class="ui-icon ui-icon-triangle-2-e-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-n"><span class="ui-icon ui-icon-arrow-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-ne"><span class="ui-icon ui-icon-arrow-1-ne"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-e"><span class="ui-icon ui-icon-arrow-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-se"><span class="ui-icon ui-icon-arrow-1-se"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-s"><span class="ui-icon ui-icon-arrow-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-sw"><span class="ui-icon ui-icon-arrow-1-sw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-w"><span class="ui-icon ui-icon-arrow-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-nw"><span class="ui-icon ui-icon-arrow-1-nw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-n-s"><span class="ui-icon ui-icon-arrow-2-n-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-ne-sw"><span class="ui-icon ui-icon-arrow-2-ne-sw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-e-w"><span class="ui-icon ui-icon-arrow-2-e-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-se-nw"><span class="ui-icon ui-icon-arrow-2-se-nw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-n"><span class="ui-icon ui-icon-arrowstop-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-e"><span class="ui-icon ui-icon-arrowstop-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-s"><span class="ui-icon ui-icon-arrowstop-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-w"><span class="ui-icon ui-icon-arrowstop-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-n"><span class="ui-icon ui-icon-arrowthick-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-ne"><span class="ui-icon ui-icon-arrowthick-1-ne"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-e"><span class="ui-icon ui-icon-arrowthick-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-se"><span class="ui-icon ui-icon-arrowthick-1-se"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-s"><span class="ui-icon ui-icon-arrowthick-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-sw"><span class="ui-icon ui-icon-arrowthick-1-sw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-w"><span class="ui-icon ui-icon-arrowthick-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-nw"><span class="ui-icon ui-icon-arrowthick-1-nw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-n-s"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-ne-sw"><span class="ui-icon ui-icon-arrowthick-2-ne-sw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-e-w"><span class="ui-icon ui-icon-arrowthick-2-e-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-se-nw"><span class="ui-icon ui-icon-arrowthick-2-se-nw"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-n"><span class="ui-icon ui-icon-arrowthickstop-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-e"><span class="ui-icon ui-icon-arrowthickstop-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-s"><span class="ui-icon ui-icon-arrowthickstop-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-w"><span class="ui-icon ui-icon-arrowthickstop-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-w"><span class="ui-icon ui-icon-arrowreturnthick-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-n"><span class="ui-icon ui-icon-arrowreturnthick-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-e"><span class="ui-icon ui-icon-arrowreturnthick-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-s"><span class="ui-icon ui-icon-arrowreturnthick-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-w"><span class="ui-icon ui-icon-arrowreturn-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-n"><span class="ui-icon ui-icon-arrowreturn-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-e"><span class="ui-icon ui-icon-arrowreturn-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-s"><span class="ui-icon ui-icon-arrowreturn-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-w"><span class="ui-icon ui-icon-arrowrefresh-1-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-n"><span class="ui-icon ui-icon-arrowrefresh-1-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-e"><span class="ui-icon ui-icon-arrowrefresh-1-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-s"><span class="ui-icon ui-icon-arrowrefresh-1-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4"><span class="ui-icon ui-icon-arrow-4"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4-diag"><span class="ui-icon ui-icon-arrow-4-diag"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-extlink"><span class="ui-icon ui-icon-extlink"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-newwin"><span class="ui-icon ui-icon-newwin"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-refresh"><span class="ui-icon ui-icon-refresh"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-shuffle"><span class="ui-icon ui-icon-shuffle"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-transfer-e-w"><span class="ui-icon ui-icon-transfer-e-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-transferthick-e-w"><span class="ui-icon ui-icon-transferthick-e-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-collapsed"><span class="ui-icon ui-icon-folder-collapsed"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-open"><span class="ui-icon ui-icon-folder-open"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-document"><span class="ui-icon ui-icon-document"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-document-b"><span class="ui-icon ui-icon-document-b"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-note"><span class="ui-icon ui-icon-note"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-closed"><span class="ui-icon ui-icon-mail-closed"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-open"><span class="ui-icon ui-icon-mail-open"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-suitcase"><span class="ui-icon ui-icon-suitcase"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-comment"><span class="ui-icon ui-icon-comment"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-person"><span class="ui-icon ui-icon-person"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-print"><span class="ui-icon ui-icon-print"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-trash"><span class="ui-icon ui-icon-trash"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-locked"><span class="ui-icon ui-icon-locked"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-unlocked"><span class="ui-icon ui-icon-unlocked"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-bookmark"><span class="ui-icon ui-icon-bookmark"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-tag"><span class="ui-icon ui-icon-tag"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-home"><span class="ui-icon ui-icon-home"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-flag"><span class="ui-icon ui-icon-flag"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-calculator"><span class="ui-icon ui-icon-calculator"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-cart"><span class="ui-icon ui-icon-cart"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-pencil"><span class="ui-icon ui-icon-pencil"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-clock"><span class="ui-icon ui-icon-clock"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-disk"><span class="ui-icon ui-icon-disk"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-calendar"><span class="ui-icon ui-icon-calendar"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomin"><span class="ui-icon ui-icon-zoomin"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomout"><span class="ui-icon ui-icon-zoomout"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-search"><span class="ui-icon ui-icon-search"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-wrench"><span class="ui-icon ui-icon-wrench"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-gear"><span class="ui-icon ui-icon-gear"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-heart"><span class="ui-icon ui-icon-heart"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-star"><span class="ui-icon ui-icon-star"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-link"><span class="ui-icon ui-icon-link"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-cancel"><span class="ui-icon ui-icon-cancel"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-plus"><span class="ui-icon ui-icon-plus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-plusthick"><span class="ui-icon ui-icon-plusthick"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-minus"><span class="ui-icon ui-icon-minus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-minusthick"><span class="ui-icon ui-icon-minusthick"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-close"><span class="ui-icon ui-icon-close"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-closethick"><span class="ui-icon ui-icon-closethick"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-key"><span class="ui-icon ui-icon-key"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-lightbulb"><span class="ui-icon ui-icon-lightbulb"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-scissors"><span class="ui-icon ui-icon-scissors"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-clipboard"><span class="ui-icon ui-icon-clipboard"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-copy"><span class="ui-icon ui-icon-copy"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-contact"><span class="ui-icon ui-icon-contact"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-image"><span class="ui-icon ui-icon-image"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-video"><span class="ui-icon ui-icon-video"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-script"><span class="ui-icon ui-icon-script"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-alert"><span class="ui-icon ui-icon-alert"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-info"><span class="ui-icon ui-icon-info"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-notice"><span class="ui-icon ui-icon-notice"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-help"><span class="ui-icon ui-icon-help"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-check"><span class="ui-icon ui-icon-check"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-bullet"><span class="ui-icon ui-icon-bullet"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-off"><span class="ui-icon ui-icon-radio-off"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-on"><span class="ui-icon ui-icon-radio-on"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-w"><span class="ui-icon ui-icon-pin-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-s"><span class="ui-icon ui-icon-pin-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-play"><span class="ui-icon ui-icon-play"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-pause"><span class="ui-icon ui-icon-pause"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-next"><span class="ui-icon ui-icon-seek-next"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-prev"><span class="ui-icon ui-icon-seek-prev"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-end"><span class="ui-icon ui-icon-seek-end"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-first"><span class="ui-icon ui-icon-seek-first"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-stop"><span class="ui-icon ui-icon-stop"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-eject"><span class="ui-icon ui-icon-eject"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-off"><span class="ui-icon ui-icon-volume-off"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-on"><span class="ui-icon ui-icon-volume-on"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-power"><span class="ui-icon ui-icon-power"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-signal-diag"><span class="ui-icon ui-icon-signal-diag"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-signal"><span class="ui-icon ui-icon-signal"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-0"><span class="ui-icon ui-icon-battery-0"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-1"><span class="ui-icon ui-icon-battery-1"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-2"><span class="ui-icon ui-icon-battery-2"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-3"><span class="ui-icon ui-icon-battery-3"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-plus"><span class="ui-icon ui-icon-circle-plus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-minus"><span class="ui-icon ui-icon-circle-minus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-close"><span class="ui-icon ui-icon-circle-close"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-e"><span class="ui-icon ui-icon-circle-triangle-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-s"><span class="ui-icon ui-icon-circle-triangle-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-w"><span class="ui-icon ui-icon-circle-triangle-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-n"><span class="ui-icon ui-icon-circle-triangle-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-e"><span class="ui-icon ui-icon-circle-arrow-e"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-s"><span class="ui-icon ui-icon-circle-arrow-s"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-w"><span class="ui-icon ui-icon-circle-arrow-w"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-n"><span class="ui-icon ui-icon-circle-arrow-n"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomin"><span class="ui-icon ui-icon-circle-zoomin"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomout"><span class="ui-icon ui-icon-circle-zoomout"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-check"><span class="ui-icon ui-icon-circle-check"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-plus"><span class="ui-icon ui-icon-circlesmall-plus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-minus"><span class="ui-icon ui-icon-circlesmall-minus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-close"><span class="ui-icon ui-icon-circlesmall-close"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-plus"><span class="ui-icon ui-icon-squaresmall-plus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-minus"><span class="ui-icon ui-icon-squaresmall-minus"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-close"><span class="ui-icon ui-icon-squaresmall-close"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-vertical"><span class="ui-icon ui-icon-grip-dotted-vertical"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-horizontal"><span class="ui-icon ui-icon-grip-dotted-horizontal"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-vertical"><span class="ui-icon ui-icon-grip-solid-vertical"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-horizontal"><span class="ui-icon ui-icon-grip-solid-horizontal"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-gripsmall-diagonal-se"><span class="ui-icon ui-icon-gripsmall-diagonal-se"></span></li>
|
||||
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-diagonal-se"><span class="ui-icon ui-icon-grip-diagonal-se"></span></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- Slider -->
|
||||
<h2 class="demoHeaders">Slider</h2>
|
||||
<div id="slider"></div>
|
||||
|
||||
|
||||
|
||||
<!-- Datepicker -->
|
||||
<h2 class="demoHeaders">Datepicker</h2>
|
||||
<div id="datepicker"></div>
|
||||
|
||||
|
||||
|
||||
<!-- Progressbar -->
|
||||
<h2 class="demoHeaders">Progressbar</h2>
|
||||
<div id="progressbar"></div>
|
||||
|
||||
|
||||
|
||||
<!-- Progressbar -->
|
||||
<h2 class="demoHeaders">Selectmenu</h2>
|
||||
<select id="selectmenu">
|
||||
<option>Slower</option>
|
||||
<option>Slow</option>
|
||||
<option selected="selected">Medium</option>
|
||||
<option>Fast</option>
|
||||
<option>Faster</option>
|
||||
</select>
|
||||
|
||||
|
||||
|
||||
<!-- Spinner -->
|
||||
<h2 class="demoHeaders">Spinner</h2>
|
||||
<input id="spinner">
|
||||
|
||||
|
||||
|
||||
<!-- Menu -->
|
||||
<h2 class="demoHeaders">Menu</h2>
|
||||
<ul style="width:100px;" id="menu">
|
||||
<li><div>Item 1</div></li>
|
||||
<li><div>Item 2</div></li>
|
||||
<li><div>Item 3</div>
|
||||
<ul>
|
||||
<li><div>Item 3-1</div></li>
|
||||
<li><div>Item 3-2</div></li>
|
||||
<li><div>Item 3-3</div></li>
|
||||
<li><div>Item 3-4</div></li>
|
||||
<li><div>Item 3-5</div></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><div>Item 4</div></li>
|
||||
<li><div>Item 5</div></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
<!-- Tooltip -->
|
||||
<h2 class="demoHeaders">Tooltip</h2>
|
||||
<p id="tooltip">
|
||||
<a href="#" title="That's what this widget is">Tooltips</a> can be attached to any element. When you hover
|
||||
the element with your mouse, the title attribute is displayed in a little box next to the element, just like a native tooltip.
|
||||
</p>
|
||||
|
||||
|
||||
<!-- Highlight / Error -->
|
||||
<h2 class="demoHeaders">Highlight / Error</h2>
|
||||
<div class="ui-widget">
|
||||
<div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0 .7em;">
|
||||
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
|
||||
<strong>Hey!</strong> Sample ui-state-highlight style.</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="ui-widget">
|
||||
<div class="ui-state-error ui-corner-all" style="padding: 0 .7em;">
|
||||
<p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>
|
||||
<strong>Alert:</strong> Sample ui-state-error style.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="external/jquery/jquery.js"></script>
|
||||
<script src="jquery-ui.js"></script>
|
||||
<script>
|
||||
|
||||
$( "#accordion" ).accordion();
|
||||
|
||||
|
||||
|
||||
var availableTags = [
|
||||
"ActionScript",
|
||||
"AppleScript",
|
||||
"Asp",
|
||||
"BASIC",
|
||||
"C",
|
||||
"C++",
|
||||
"Clojure",
|
||||
"COBOL",
|
||||
"ColdFusion",
|
||||
"Erlang",
|
||||
"Fortran",
|
||||
"Groovy",
|
||||
"Haskell",
|
||||
"Java",
|
||||
"JavaScript",
|
||||
"Lisp",
|
||||
"Perl",
|
||||
"PHP",
|
||||
"Python",
|
||||
"Ruby",
|
||||
"Scala",
|
||||
"Scheme"
|
||||
];
|
||||
$( "#autocomplete" ).autocomplete({
|
||||
source: availableTags
|
||||
});
|
||||
|
||||
|
||||
|
||||
$( "#button" ).button();
|
||||
$( "#button-icon" ).button({
|
||||
icon: "ui-icon-gear",
|
||||
showLabel: false
|
||||
});
|
||||
|
||||
|
||||
|
||||
$( "#radioset" ).buttonset();
|
||||
|
||||
|
||||
|
||||
$( "#controlgroup" ).controlgroup();
|
||||
|
||||
|
||||
|
||||
$( "#tabs" ).tabs();
|
||||
|
||||
|
||||
|
||||
$( "#dialog" ).dialog({
|
||||
autoOpen: false,
|
||||
width: 400,
|
||||
buttons: [
|
||||
{
|
||||
text: "Ok",
|
||||
click: function() {
|
||||
$( this ).dialog( "close" );
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "Cancel",
|
||||
click: function() {
|
||||
$( this ).dialog( "close" );
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Link to open the dialog
|
||||
$( "#dialog-link" ).click(function( event ) {
|
||||
$( "#dialog" ).dialog( "open" );
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
|
||||
|
||||
$( "#datepicker" ).datepicker({
|
||||
inline: true
|
||||
});
|
||||
|
||||
|
||||
|
||||
$( "#slider" ).slider({
|
||||
range: true,
|
||||
values: [ 17, 67 ]
|
||||
});
|
||||
|
||||
|
||||
|
||||
$( "#progressbar" ).progressbar({
|
||||
value: 20
|
||||
});
|
||||
|
||||
|
||||
|
||||
$( "#spinner" ).spinner();
|
||||
|
||||
|
||||
|
||||
$( "#menu" ).menu();
|
||||
|
||||
|
||||
|
||||
$( "#tooltip" ).tooltip();
|
||||
|
||||
|
||||
|
||||
$( "#selectmenu" ).selectmenu();
|
||||
|
||||
|
||||
// Hover states on the static widgets
|
||||
$( "#dialog-link, #icons li" ).hover(
|
||||
function() {
|
||||
$( this ).addClass( "ui-state-hover" );
|
||||
},
|
||||
function() {
|
||||
$( this ).removeClass( "ui-state-hover" );
|
||||
}
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,886 @@
|
||||
/*!
|
||||
* jQuery UI CSS Framework 1.12.1
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/category/theming/
|
||||
*/
|
||||
.ui-draggable-handle {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
/* Layout helpers
|
||||
----------------------------------*/
|
||||
.ui-helper-hidden {
|
||||
display: none;
|
||||
}
|
||||
.ui-helper-hidden-accessible {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
.ui-helper-reset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
line-height: 1.3;
|
||||
text-decoration: none;
|
||||
font-size: 100%;
|
||||
list-style: none;
|
||||
}
|
||||
.ui-helper-clearfix:before,
|
||||
.ui-helper-clearfix:after {
|
||||
content: "";
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.ui-helper-clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
.ui-helper-zfix {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
filter:Alpha(Opacity=0); /* support: IE8 */
|
||||
}
|
||||
|
||||
.ui-front {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
/* Interaction Cues
|
||||
----------------------------------*/
|
||||
.ui-state-disabled {
|
||||
cursor: default !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
/* Icons
|
||||
----------------------------------*/
|
||||
.ui-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-top: -.25em;
|
||||
position: relative;
|
||||
text-indent: -99999px;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.ui-widget-icon-block {
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Misc visuals
|
||||
----------------------------------*/
|
||||
|
||||
/* Overlays */
|
||||
.ui-widget-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.ui-resizable {
|
||||
position: relative;
|
||||
}
|
||||
.ui-resizable-handle {
|
||||
position: absolute;
|
||||
font-size: 0.1px;
|
||||
display: block;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.ui-resizable-disabled .ui-resizable-handle,
|
||||
.ui-resizable-autohide .ui-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
.ui-resizable-n {
|
||||
cursor: n-resize;
|
||||
height: 7px;
|
||||
width: 100%;
|
||||
top: -5px;
|
||||
left: 0;
|
||||
}
|
||||
.ui-resizable-s {
|
||||
cursor: s-resize;
|
||||
height: 7px;
|
||||
width: 100%;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
}
|
||||
.ui-resizable-e {
|
||||
cursor: e-resize;
|
||||
width: 7px;
|
||||
right: -5px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.ui-resizable-w {
|
||||
cursor: w-resize;
|
||||
width: 7px;
|
||||
left: -5px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.ui-resizable-se {
|
||||
cursor: se-resize;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
}
|
||||
.ui-resizable-sw {
|
||||
cursor: sw-resize;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
left: -5px;
|
||||
bottom: -5px;
|
||||
}
|
||||
.ui-resizable-nw {
|
||||
cursor: nw-resize;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
left: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-resizable-ne {
|
||||
cursor: ne-resize;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-selectable {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.ui-selectable-helper {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
border: 1px dotted black;
|
||||
}
|
||||
.ui-sortable-handle {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.ui-accordion .ui-accordion-header {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin: 2px 0 0 0;
|
||||
padding: .5em .5em .5em .7em;
|
||||
font-size: 100%;
|
||||
}
|
||||
.ui-accordion .ui-accordion-content {
|
||||
padding: 1em 2.2em;
|
||||
border-top: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ui-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* support: IE10, see #8844 */
|
||||
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||
}
|
||||
.ui-menu .ui-menu-item-wrapper {
|
||||
position: relative;
|
||||
padding: 3px 1em 3px .4em;
|
||||
}
|
||||
.ui-menu .ui-menu-divider {
|
||||
margin: 5px 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.ui-menu .ui-state-focus,
|
||||
.ui-menu .ui-state-active {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
/* icon support */
|
||||
.ui-menu-icons {
|
||||
position: relative;
|
||||
}
|
||||
.ui-menu-icons .ui-menu-item-wrapper {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* left-aligned */
|
||||
.ui-menu .ui-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: .2em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* right-aligned */
|
||||
.ui-menu .ui-menu-icon {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
.ui-button {
|
||||
padding: .4em 1em;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
margin-right: .1em;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
/* Support: IE <= 11 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ui-button,
|
||||
.ui-button:link,
|
||||
.ui-button:visited,
|
||||
.ui-button:hover,
|
||||
.ui-button:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* to make room for the icon, a width needs to be set here */
|
||||
.ui-button-icon-only {
|
||||
width: 2em;
|
||||
box-sizing: border-box;
|
||||
text-indent: -9999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* no icon support for input elements */
|
||||
input.ui-button.ui-button-icon-only {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
/* button icon element(s) */
|
||||
.ui-button-icon-only .ui-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.ui-button.ui-icon-notext .ui-icon {
|
||||
padding: 0;
|
||||
width: 2.1em;
|
||||
height: 2.1em;
|
||||
text-indent: -9999px;
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
|
||||
input.ui-button.ui-icon-notext .ui-icon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-indent: 0;
|
||||
white-space: normal;
|
||||
padding: .4em 1em;
|
||||
}
|
||||
|
||||
/* workarounds */
|
||||
/* Support: Firefox 5 - 40 */
|
||||
input.ui-button::-moz-focus-inner,
|
||||
button.ui-button::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ui-controlgroup {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
.ui-controlgroup > .ui-controlgroup-item {
|
||||
float: left;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.ui-controlgroup > .ui-controlgroup-item:focus,
|
||||
.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus {
|
||||
z-index: 9999;
|
||||
}
|
||||
.ui-controlgroup-vertical > .ui-controlgroup-item {
|
||||
display: block;
|
||||
float: none;
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.ui-controlgroup-vertical .ui-controlgroup-item {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ui-controlgroup .ui-controlgroup-label {
|
||||
padding: .4em 1em;
|
||||
}
|
||||
.ui-controlgroup .ui-controlgroup-label span {
|
||||
font-size: 80%;
|
||||
}
|
||||
.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item {
|
||||
border-left: none;
|
||||
}
|
||||
.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item {
|
||||
border-top: none;
|
||||
}
|
||||
.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content {
|
||||
border-right: none;
|
||||
}
|
||||
.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Spinner specific style fixes */
|
||||
.ui-controlgroup-vertical .ui-spinner-input {
|
||||
|
||||
/* Support: IE8 only, Android < 4.4 only */
|
||||
width: 75%;
|
||||
width: calc( 100% - 2.4em );
|
||||
}
|
||||
.ui-controlgroup-vertical .ui-spinner .ui-spinner-up {
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.ui-checkboxradio-label .ui-icon-background {
|
||||
box-shadow: inset 1px 1px 1px #ccc;
|
||||
border-radius: .12em;
|
||||
border: none;
|
||||
}
|
||||
.ui-checkboxradio-radio-label .ui-icon-background {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 1em;
|
||||
overflow: visible;
|
||||
border: none;
|
||||
}
|
||||
.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,
|
||||
.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon {
|
||||
background-image: none;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
}
|
||||
.ui-checkboxradio-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
.ui-datepicker {
|
||||
width: 17em;
|
||||
padding: .2em .2em 0;
|
||||
display: none;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-header {
|
||||
position: relative;
|
||||
padding: .2em 0;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev,
|
||||
.ui-datepicker .ui-datepicker-next {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev-hover,
|
||||
.ui-datepicker .ui-datepicker-next-hover {
|
||||
top: 1px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev {
|
||||
left: 2px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-next {
|
||||
right: 2px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev-hover {
|
||||
left: 1px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-next-hover {
|
||||
right: 1px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev span,
|
||||
.ui-datepicker .ui-datepicker-next span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-title {
|
||||
margin: 0 2.3em;
|
||||
line-height: 1.8em;
|
||||
text-align: center;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-title select {
|
||||
font-size: 1em;
|
||||
margin: 1px 0;
|
||||
}
|
||||
.ui-datepicker select.ui-datepicker-month,
|
||||
.ui-datepicker select.ui-datepicker-year {
|
||||
width: 45%;
|
||||
}
|
||||
.ui-datepicker table {
|
||||
width: 100%;
|
||||
font-size: .9em;
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 .4em;
|
||||
}
|
||||
.ui-datepicker th {
|
||||
padding: .7em .3em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border: 0;
|
||||
}
|
||||
.ui-datepicker td {
|
||||
border: 0;
|
||||
padding: 1px;
|
||||
}
|
||||
.ui-datepicker td span,
|
||||
.ui-datepicker td a {
|
||||
display: block;
|
||||
padding: .2em;
|
||||
text-align: right;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-buttonpane {
|
||||
background-image: none;
|
||||
margin: .7em 0 0 0;
|
||||
padding: 0 .2em;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-buttonpane button {
|
||||
float: right;
|
||||
margin: .5em .2em .4em;
|
||||
cursor: pointer;
|
||||
padding: .2em .6em .3em .6em;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* with multiple calendars */
|
||||
.ui-datepicker.ui-datepicker-multi {
|
||||
width: auto;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-group {
|
||||
float: left;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-group table {
|
||||
width: 95%;
|
||||
margin: 0 auto .4em;
|
||||
}
|
||||
.ui-datepicker-multi-2 .ui-datepicker-group {
|
||||
width: 50%;
|
||||
}
|
||||
.ui-datepicker-multi-3 .ui-datepicker-group {
|
||||
width: 33.3%;
|
||||
}
|
||||
.ui-datepicker-multi-4 .ui-datepicker-group {
|
||||
width: 25%;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
|
||||
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
|
||||
border-left-width: 0;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-buttonpane {
|
||||
clear: left;
|
||||
}
|
||||
.ui-datepicker-row-break {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
/* RTL support */
|
||||
.ui-datepicker-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-prev {
|
||||
right: 2px;
|
||||
left: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-next {
|
||||
left: 2px;
|
||||
right: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-prev:hover {
|
||||
right: 1px;
|
||||
left: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-next:hover {
|
||||
left: 1px;
|
||||
right: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane {
|
||||
clear: right;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane button {
|
||||
float: left;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
|
||||
.ui-datepicker-rtl .ui-datepicker-group {
|
||||
float: right;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
|
||||
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
|
||||
border-right-width: 0;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.ui-datepicker .ui-icon {
|
||||
display: block;
|
||||
text-indent: -99999px;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
left: .5em;
|
||||
top: .3em;
|
||||
}
|
||||
.ui-dialog {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: .2em;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-dialog .ui-dialog-titlebar {
|
||||
padding: .4em 1em;
|
||||
position: relative;
|
||||
}
|
||||
.ui-dialog .ui-dialog-title {
|
||||
float: left;
|
||||
margin: .1em 0;
|
||||
white-space: nowrap;
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ui-dialog .ui-dialog-titlebar-close {
|
||||
position: absolute;
|
||||
right: .3em;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
margin: -10px 0 0 0;
|
||||
padding: 1px;
|
||||
height: 20px;
|
||||
}
|
||||
.ui-dialog .ui-dialog-content {
|
||||
position: relative;
|
||||
border: 0;
|
||||
padding: .5em 1em;
|
||||
background: none;
|
||||
overflow: auto;
|
||||
}
|
||||
.ui-dialog .ui-dialog-buttonpane {
|
||||
text-align: left;
|
||||
border-width: 1px 0 0 0;
|
||||
background-image: none;
|
||||
margin-top: .5em;
|
||||
padding: .3em 1em .5em .4em;
|
||||
}
|
||||
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
|
||||
float: right;
|
||||
}
|
||||
.ui-dialog .ui-dialog-buttonpane button {
|
||||
margin: .5em .4em .5em 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-dialog .ui-resizable-n {
|
||||
height: 2px;
|
||||
top: 0;
|
||||
}
|
||||
.ui-dialog .ui-resizable-e {
|
||||
width: 2px;
|
||||
right: 0;
|
||||
}
|
||||
.ui-dialog .ui-resizable-s {
|
||||
height: 2px;
|
||||
bottom: 0;
|
||||
}
|
||||
.ui-dialog .ui-resizable-w {
|
||||
width: 2px;
|
||||
left: 0;
|
||||
}
|
||||
.ui-dialog .ui-resizable-se,
|
||||
.ui-dialog .ui-resizable-sw,
|
||||
.ui-dialog .ui-resizable-ne,
|
||||
.ui-dialog .ui-resizable-nw {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
.ui-dialog .ui-resizable-se {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.ui-dialog .ui-resizable-sw {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.ui-dialog .ui-resizable-ne {
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.ui-dialog .ui-resizable-nw {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.ui-draggable .ui-dialog-titlebar {
|
||||
cursor: move;
|
||||
}
|
||||
.ui-progressbar {
|
||||
height: 2em;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-progressbar .ui-progressbar-value {
|
||||
margin: -1px;
|
||||
height: 100%;
|
||||
}
|
||||
.ui-progressbar .ui-progressbar-overlay {
|
||||
background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");
|
||||
height: 100%;
|
||||
filter: alpha(opacity=25); /* support: IE8 */
|
||||
opacity: 0.25;
|
||||
}
|
||||
.ui-progressbar-indeterminate .ui-progressbar-value {
|
||||
background-image: none;
|
||||
}
|
||||
.ui-selectmenu-menu {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
.ui-selectmenu-menu .ui-menu {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
line-height: 1.5;
|
||||
padding: 2px 0.4em;
|
||||
margin: 0.5em 0 0 0;
|
||||
height: auto;
|
||||
border: 0;
|
||||
}
|
||||
.ui-selectmenu-open {
|
||||
display: block;
|
||||
}
|
||||
.ui-selectmenu-text {
|
||||
display: block;
|
||||
margin-right: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ui-selectmenu-button.ui-button {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
width: 14em;
|
||||
}
|
||||
.ui-selectmenu-icon.ui-icon {
|
||||
float: right;
|
||||
margin-top: 0;
|
||||
}
|
||||
.ui-slider {
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
.ui-slider .ui-slider-handle {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
cursor: default;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.ui-slider .ui-slider-range {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
font-size: .7em;
|
||||
display: block;
|
||||
border: 0;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
/* support: IE8 - See #6727 */
|
||||
.ui-slider.ui-state-disabled .ui-slider-handle,
|
||||
.ui-slider.ui-state-disabled .ui-slider-range {
|
||||
filter: inherit;
|
||||
}
|
||||
|
||||
.ui-slider-horizontal {
|
||||
height: .8em;
|
||||
}
|
||||
.ui-slider-horizontal .ui-slider-handle {
|
||||
top: -.3em;
|
||||
margin-left: -.6em;
|
||||
}
|
||||
.ui-slider-horizontal .ui-slider-range {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.ui-slider-horizontal .ui-slider-range-min {
|
||||
left: 0;
|
||||
}
|
||||
.ui-slider-horizontal .ui-slider-range-max {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ui-slider-vertical {
|
||||
width: .8em;
|
||||
height: 100px;
|
||||
}
|
||||
.ui-slider-vertical .ui-slider-handle {
|
||||
left: -.3em;
|
||||
margin-left: 0;
|
||||
margin-bottom: -.6em;
|
||||
}
|
||||
.ui-slider-vertical .ui-slider-range {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.ui-slider-vertical .ui-slider-range-min {
|
||||
bottom: 0;
|
||||
}
|
||||
.ui-slider-vertical .ui-slider-range-max {
|
||||
top: 0;
|
||||
}
|
||||
.ui-spinner {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ui-spinner-input {
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
padding: .222em 0;
|
||||
margin: .2em 0;
|
||||
vertical-align: middle;
|
||||
margin-left: .4em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
.ui-spinner-button {
|
||||
width: 1.6em;
|
||||
height: 50%;
|
||||
font-size: .5em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
right: 0;
|
||||
}
|
||||
/* more specificity required here to override default borders */
|
||||
.ui-spinner a.ui-spinner-button {
|
||||
border-top-style: none;
|
||||
border-bottom-style: none;
|
||||
border-right-style: none;
|
||||
}
|
||||
.ui-spinner-up {
|
||||
top: 0;
|
||||
}
|
||||
.ui-spinner-down {
|
||||
bottom: 0;
|
||||
}
|
||||
.ui-tabs {
|
||||
position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
|
||||
padding: .2em;
|
||||
}
|
||||
.ui-tabs .ui-tabs-nav {
|
||||
margin: 0;
|
||||
padding: .2em .2em 0;
|
||||
}
|
||||
.ui-tabs .ui-tabs-nav li {
|
||||
list-style: none;
|
||||
float: left;
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin: 1px .2em 0 0;
|
||||
border-bottom-width: 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
|
||||
float: left;
|
||||
padding: .5em 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ui-tabs .ui-tabs-nav li.ui-tabs-active {
|
||||
margin-bottom: -1px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
|
||||
.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
|
||||
.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
|
||||
cursor: text;
|
||||
}
|
||||
.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-tabs .ui-tabs-panel {
|
||||
display: block;
|
||||
border-width: 0;
|
||||
padding: 1em 1.4em;
|
||||
background: none;
|
||||
}
|
||||
.ui-tooltip {
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
max-width: 300px;
|
||||
}
|
||||
body .ui-tooltip {
|
||||
border-width: 2px;
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
/*!
|
||||
* jQuery UI CSS Framework 1.12.1
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/category/theming/
|
||||
*
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
|
||||
*/
|
||||
|
||||
|
||||
/* Component containers
|
||||
----------------------------------*/
|
||||
.ui-widget {
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
.ui-widget .ui-widget {
|
||||
font-size: 1em;
|
||||
}
|
||||
.ui-widget input,
|
||||
.ui-widget select,
|
||||
.ui-widget textarea,
|
||||
.ui-widget button {
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
.ui-widget.ui-widget-content {
|
||||
border: 1px solid #c5c5c5;
|
||||
}
|
||||
.ui-widget-content {
|
||||
border: 1px solid #dddddd;
|
||||
background: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
.ui-widget-content a {
|
||||
color: #333333;
|
||||
}
|
||||
.ui-widget-header {
|
||||
border: 1px solid #dddddd;
|
||||
background: #e9e9e9;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.ui-widget-header a {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Interaction states
|
||||
----------------------------------*/
|
||||
.ui-state-default,
|
||||
.ui-widget-content .ui-state-default,
|
||||
.ui-widget-header .ui-state-default,
|
||||
.ui-button,
|
||||
|
||||
/* We use html here because we need a greater specificity to make sure disabled
|
||||
works properly when clicked or hovered */
|
||||
html .ui-button.ui-state-disabled:hover,
|
||||
html .ui-button.ui-state-disabled:active {
|
||||
border: 1px solid #c5c5c5;
|
||||
background: #f6f6f6;
|
||||
font-weight: normal;
|
||||
color: #454545;
|
||||
}
|
||||
.ui-state-default a,
|
||||
.ui-state-default a:link,
|
||||
.ui-state-default a:visited,
|
||||
a.ui-button,
|
||||
a:link.ui-button,
|
||||
a:visited.ui-button,
|
||||
.ui-button {
|
||||
color: #454545;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ui-state-hover,
|
||||
.ui-widget-content .ui-state-hover,
|
||||
.ui-widget-header .ui-state-hover,
|
||||
.ui-state-focus,
|
||||
.ui-widget-content .ui-state-focus,
|
||||
.ui-widget-header .ui-state-focus,
|
||||
.ui-button:hover,
|
||||
.ui-button:focus {
|
||||
border: 1px solid #cccccc;
|
||||
background: #ededed;
|
||||
font-weight: normal;
|
||||
color: #2b2b2b;
|
||||
}
|
||||
.ui-state-hover a,
|
||||
.ui-state-hover a:hover,
|
||||
.ui-state-hover a:link,
|
||||
.ui-state-hover a:visited,
|
||||
.ui-state-focus a,
|
||||
.ui-state-focus a:hover,
|
||||
.ui-state-focus a:link,
|
||||
.ui-state-focus a:visited,
|
||||
a.ui-button:hover,
|
||||
a.ui-button:focus {
|
||||
color: #2b2b2b;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ui-visual-focus {
|
||||
box-shadow: 0 0 3px 1px rgb(94, 158, 214);
|
||||
}
|
||||
.ui-state-active,
|
||||
.ui-widget-content .ui-state-active,
|
||||
.ui-widget-header .ui-state-active,
|
||||
a.ui-button:active,
|
||||
.ui-button:active,
|
||||
.ui-button.ui-state-active:hover {
|
||||
border: 1px solid #003eff;
|
||||
background: #007fff;
|
||||
font-weight: normal;
|
||||
color: #ffffff;
|
||||
}
|
||||
.ui-icon-background,
|
||||
.ui-state-active .ui-icon-background {
|
||||
border: #003eff;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.ui-state-active a,
|
||||
.ui-state-active a:link,
|
||||
.ui-state-active a:visited {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Interaction Cues
|
||||
----------------------------------*/
|
||||
.ui-state-highlight,
|
||||
.ui-widget-content .ui-state-highlight,
|
||||
.ui-widget-header .ui-state-highlight {
|
||||
border: 1px solid #dad55e;
|
||||
background: #fffa90;
|
||||
color: #777620;
|
||||
}
|
||||
.ui-state-checked {
|
||||
border: 1px solid #dad55e;
|
||||
background: #fffa90;
|
||||
}
|
||||
.ui-state-highlight a,
|
||||
.ui-widget-content .ui-state-highlight a,
|
||||
.ui-widget-header .ui-state-highlight a {
|
||||
color: #777620;
|
||||
}
|
||||
.ui-state-error,
|
||||
.ui-widget-content .ui-state-error,
|
||||
.ui-widget-header .ui-state-error {
|
||||
border: 1px solid #f1a899;
|
||||
background: #fddfdf;
|
||||
color: #5f3f3f;
|
||||
}
|
||||
.ui-state-error a,
|
||||
.ui-widget-content .ui-state-error a,
|
||||
.ui-widget-header .ui-state-error a {
|
||||
color: #5f3f3f;
|
||||
}
|
||||
.ui-state-error-text,
|
||||
.ui-widget-content .ui-state-error-text,
|
||||
.ui-widget-header .ui-state-error-text {
|
||||
color: #5f3f3f;
|
||||
}
|
||||
.ui-priority-primary,
|
||||
.ui-widget-content .ui-priority-primary,
|
||||
.ui-widget-header .ui-priority-primary {
|
||||
font-weight: bold;
|
||||
}
|
||||
.ui-priority-secondary,
|
||||
.ui-widget-content .ui-priority-secondary,
|
||||
.ui-widget-header .ui-priority-secondary {
|
||||
opacity: .7;
|
||||
filter:Alpha(Opacity=70); /* support: IE8 */
|
||||
font-weight: normal;
|
||||
}
|
||||
.ui-state-disabled,
|
||||
.ui-widget-content .ui-state-disabled,
|
||||
.ui-widget-header .ui-state-disabled {
|
||||
opacity: .35;
|
||||
filter:Alpha(Opacity=35); /* support: IE8 */
|
||||
background-image: none;
|
||||
}
|
||||
.ui-state-disabled .ui-icon {
|
||||
filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
|
||||
}
|
||||
|
||||
/* Icons
|
||||
----------------------------------*/
|
||||
|
||||
/* states and images */
|
||||
.ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.ui-icon,
|
||||
.ui-widget-content .ui-icon {
|
||||
background-image: url("images/ui-icons_444444_256x240.png");
|
||||
}
|
||||
.ui-widget-header .ui-icon {
|
||||
background-image: url("images/ui-icons_444444_256x240.png");
|
||||
}
|
||||
.ui-state-hover .ui-icon,
|
||||
.ui-state-focus .ui-icon,
|
||||
.ui-button:hover .ui-icon,
|
||||
.ui-button:focus .ui-icon {
|
||||
background-image: url("images/ui-icons_555555_256x240.png");
|
||||
}
|
||||
.ui-state-active .ui-icon,
|
||||
.ui-button:active .ui-icon {
|
||||
background-image: url("images/ui-icons_ffffff_256x240.png");
|
||||
}
|
||||
.ui-state-highlight .ui-icon,
|
||||
.ui-button .ui-state-highlight.ui-icon {
|
||||
background-image: url("images/ui-icons_777620_256x240.png");
|
||||
}
|
||||
.ui-state-error .ui-icon,
|
||||
.ui-state-error-text .ui-icon {
|
||||
background-image: url("images/ui-icons_cc0000_256x240.png");
|
||||
}
|
||||
.ui-button .ui-icon {
|
||||
background-image: url("images/ui-icons_777777_256x240.png");
|
||||
}
|
||||
|
||||
/* positioning */
|
||||
.ui-icon-blank { background-position: 16px 16px; }
|
||||
.ui-icon-caret-1-n { background-position: 0 0; }
|
||||
.ui-icon-caret-1-ne { background-position: -16px 0; }
|
||||
.ui-icon-caret-1-e { background-position: -32px 0; }
|
||||
.ui-icon-caret-1-se { background-position: -48px 0; }
|
||||
.ui-icon-caret-1-s { background-position: -65px 0; }
|
||||
.ui-icon-caret-1-sw { background-position: -80px 0; }
|
||||
.ui-icon-caret-1-w { background-position: -96px 0; }
|
||||
.ui-icon-caret-1-nw { background-position: -112px 0; }
|
||||
.ui-icon-caret-2-n-s { background-position: -128px 0; }
|
||||
.ui-icon-caret-2-e-w { background-position: -144px 0; }
|
||||
.ui-icon-triangle-1-n { background-position: 0 -16px; }
|
||||
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
|
||||
.ui-icon-triangle-1-e { background-position: -32px -16px; }
|
||||
.ui-icon-triangle-1-se { background-position: -48px -16px; }
|
||||
.ui-icon-triangle-1-s { background-position: -65px -16px; }
|
||||
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
|
||||
.ui-icon-triangle-1-w { background-position: -96px -16px; }
|
||||
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
|
||||
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
|
||||
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
|
||||
.ui-icon-arrow-1-n { background-position: 0 -32px; }
|
||||
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
|
||||
.ui-icon-arrow-1-e { background-position: -32px -32px; }
|
||||
.ui-icon-arrow-1-se { background-position: -48px -32px; }
|
||||
.ui-icon-arrow-1-s { background-position: -65px -32px; }
|
||||
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
|
||||
.ui-icon-arrow-1-w { background-position: -96px -32px; }
|
||||
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
|
||||
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
|
||||
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
|
||||
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
|
||||
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
|
||||
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
|
||||
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
|
||||
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
|
||||
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
|
||||
.ui-icon-arrowthick-1-n { background-position: 1px -48px; }
|
||||
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
|
||||
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
|
||||
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
|
||||
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
|
||||
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
|
||||
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
|
||||
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
|
||||
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
|
||||
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
|
||||
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
|
||||
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
|
||||
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
|
||||
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
|
||||
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
|
||||
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
|
||||
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
|
||||
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
|
||||
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
|
||||
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
|
||||
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
|
||||
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
|
||||
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
|
||||
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
|
||||
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
|
||||
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
|
||||
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
|
||||
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
|
||||
.ui-icon-arrow-4 { background-position: 0 -80px; }
|
||||
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
|
||||
.ui-icon-extlink { background-position: -32px -80px; }
|
||||
.ui-icon-newwin { background-position: -48px -80px; }
|
||||
.ui-icon-refresh { background-position: -64px -80px; }
|
||||
.ui-icon-shuffle { background-position: -80px -80px; }
|
||||
.ui-icon-transfer-e-w { background-position: -96px -80px; }
|
||||
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
|
||||
.ui-icon-folder-collapsed { background-position: 0 -96px; }
|
||||
.ui-icon-folder-open { background-position: -16px -96px; }
|
||||
.ui-icon-document { background-position: -32px -96px; }
|
||||
.ui-icon-document-b { background-position: -48px -96px; }
|
||||
.ui-icon-note { background-position: -64px -96px; }
|
||||
.ui-icon-mail-closed { background-position: -80px -96px; }
|
||||
.ui-icon-mail-open { background-position: -96px -96px; }
|
||||
.ui-icon-suitcase { background-position: -112px -96px; }
|
||||
.ui-icon-comment { background-position: -128px -96px; }
|
||||
.ui-icon-person { background-position: -144px -96px; }
|
||||
.ui-icon-print { background-position: -160px -96px; }
|
||||
.ui-icon-trash { background-position: -176px -96px; }
|
||||
.ui-icon-locked { background-position: -192px -96px; }
|
||||
.ui-icon-unlocked { background-position: -208px -96px; }
|
||||
.ui-icon-bookmark { background-position: -224px -96px; }
|
||||
.ui-icon-tag { background-position: -240px -96px; }
|
||||
.ui-icon-home { background-position: 0 -112px; }
|
||||
.ui-icon-flag { background-position: -16px -112px; }
|
||||
.ui-icon-calendar { background-position: -32px -112px; }
|
||||
.ui-icon-cart { background-position: -48px -112px; }
|
||||
.ui-icon-pencil { background-position: -64px -112px; }
|
||||
.ui-icon-clock { background-position: -80px -112px; }
|
||||
.ui-icon-disk { background-position: -96px -112px; }
|
||||
.ui-icon-calculator { background-position: -112px -112px; }
|
||||
.ui-icon-zoomin { background-position: -128px -112px; }
|
||||
.ui-icon-zoomout { background-position: -144px -112px; }
|
||||
.ui-icon-search { background-position: -160px -112px; }
|
||||
.ui-icon-wrench { background-position: -176px -112px; }
|
||||
.ui-icon-gear { background-position: -192px -112px; }
|
||||
.ui-icon-heart { background-position: -208px -112px; }
|
||||
.ui-icon-star { background-position: -224px -112px; }
|
||||
.ui-icon-link { background-position: -240px -112px; }
|
||||
.ui-icon-cancel { background-position: 0 -128px; }
|
||||
.ui-icon-plus { background-position: -16px -128px; }
|
||||
.ui-icon-plusthick { background-position: -32px -128px; }
|
||||
.ui-icon-minus { background-position: -48px -128px; }
|
||||
.ui-icon-minusthick { background-position: -64px -128px; }
|
||||
.ui-icon-close { background-position: -80px -128px; }
|
||||
.ui-icon-closethick { background-position: -96px -128px; }
|
||||
.ui-icon-key { background-position: -112px -128px; }
|
||||
.ui-icon-lightbulb { background-position: -128px -128px; }
|
||||
.ui-icon-scissors { background-position: -144px -128px; }
|
||||
.ui-icon-clipboard { background-position: -160px -128px; }
|
||||
.ui-icon-copy { background-position: -176px -128px; }
|
||||
.ui-icon-contact { background-position: -192px -128px; }
|
||||
.ui-icon-image { background-position: -208px -128px; }
|
||||
.ui-icon-video { background-position: -224px -128px; }
|
||||
.ui-icon-script { background-position: -240px -128px; }
|
||||
.ui-icon-alert { background-position: 0 -144px; }
|
||||
.ui-icon-info { background-position: -16px -144px; }
|
||||
.ui-icon-notice { background-position: -32px -144px; }
|
||||
.ui-icon-help { background-position: -48px -144px; }
|
||||
.ui-icon-check { background-position: -64px -144px; }
|
||||
.ui-icon-bullet { background-position: -80px -144px; }
|
||||
.ui-icon-radio-on { background-position: -96px -144px; }
|
||||
.ui-icon-radio-off { background-position: -112px -144px; }
|
||||
.ui-icon-pin-w { background-position: -128px -144px; }
|
||||
.ui-icon-pin-s { background-position: -144px -144px; }
|
||||
.ui-icon-play { background-position: 0 -160px; }
|
||||
.ui-icon-pause { background-position: -16px -160px; }
|
||||
.ui-icon-seek-next { background-position: -32px -160px; }
|
||||
.ui-icon-seek-prev { background-position: -48px -160px; }
|
||||
.ui-icon-seek-end { background-position: -64px -160px; }
|
||||
.ui-icon-seek-start { background-position: -80px -160px; }
|
||||
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
|
||||
.ui-icon-seek-first { background-position: -80px -160px; }
|
||||
.ui-icon-stop { background-position: -96px -160px; }
|
||||
.ui-icon-eject { background-position: -112px -160px; }
|
||||
.ui-icon-volume-off { background-position: -128px -160px; }
|
||||
.ui-icon-volume-on { background-position: -144px -160px; }
|
||||
.ui-icon-power { background-position: 0 -176px; }
|
||||
.ui-icon-signal-diag { background-position: -16px -176px; }
|
||||
.ui-icon-signal { background-position: -32px -176px; }
|
||||
.ui-icon-battery-0 { background-position: -48px -176px; }
|
||||
.ui-icon-battery-1 { background-position: -64px -176px; }
|
||||
.ui-icon-battery-2 { background-position: -80px -176px; }
|
||||
.ui-icon-battery-3 { background-position: -96px -176px; }
|
||||
.ui-icon-circle-plus { background-position: 0 -192px; }
|
||||
.ui-icon-circle-minus { background-position: -16px -192px; }
|
||||
.ui-icon-circle-close { background-position: -32px -192px; }
|
||||
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
|
||||
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
|
||||
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
|
||||
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
|
||||
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
|
||||
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
|
||||
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
|
||||
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
|
||||
.ui-icon-circle-zoomin { background-position: -176px -192px; }
|
||||
.ui-icon-circle-zoomout { background-position: -192px -192px; }
|
||||
.ui-icon-circle-check { background-position: -208px -192px; }
|
||||
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
|
||||
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
|
||||
.ui-icon-circlesmall-close { background-position: -32px -208px; }
|
||||
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
|
||||
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
|
||||
.ui-icon-squaresmall-close { background-position: -80px -208px; }
|
||||
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
|
||||
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
|
||||
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
|
||||
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
|
||||
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
|
||||
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
|
||||
|
||||
|
||||
/* Misc visuals
|
||||
----------------------------------*/
|
||||
|
||||
/* Corner radius */
|
||||
.ui-corner-all,
|
||||
.ui-corner-top,
|
||||
.ui-corner-left,
|
||||
.ui-corner-tl {
|
||||
border-top-left-radius: 3px;
|
||||
}
|
||||
.ui-corner-all,
|
||||
.ui-corner-top,
|
||||
.ui-corner-right,
|
||||
.ui-corner-tr {
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
.ui-corner-all,
|
||||
.ui-corner-bottom,
|
||||
.ui-corner-left,
|
||||
.ui-corner-bl {
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
.ui-corner-all,
|
||||
.ui-corner-bottom,
|
||||
.ui-corner-right,
|
||||
.ui-corner-br {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
/* Overlays */
|
||||
.ui-widget-overlay {
|
||||
background: #aaaaaa;
|
||||
opacity: .3;
|
||||
filter: Alpha(Opacity=30); /* support: IE8 */
|
||||
}
|
||||
.ui-widget-shadow {
|
||||
-webkit-box-shadow: 0px 0px 5px #666666;
|
||||
box-shadow: 0px 0px 5px #666666;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"author": {
|
||||
"name": "jQuery Foundation and other contributors",
|
||||
"url": "https://github.com/jquery/jquery-ui/blob/1.12.1/AUTHORS.txt"
|
||||
},
|
||||
"bugs": "https://bugs.jqueryui.com/",
|
||||
"dependencies": {},
|
||||
"description": "A curated set of user interface interactions, effects, widgets, and themes built on top of the jQuery JavaScript Library.",
|
||||
"devDependencies": {
|
||||
"commitplease": "2.3.0",
|
||||
"grunt": "0.4.5",
|
||||
"grunt-bowercopy": "1.2.4",
|
||||
"grunt-cli": "0.1.13",
|
||||
"grunt-compare-size": "0.4.0",
|
||||
"grunt-contrib-concat": "0.5.1",
|
||||
"grunt-contrib-csslint": "0.5.0",
|
||||
"grunt-contrib-jshint": "0.12.0",
|
||||
"grunt-contrib-qunit": "1.0.1",
|
||||
"grunt-contrib-requirejs": "0.4.4",
|
||||
"grunt-contrib-uglify": "0.11.1",
|
||||
"grunt-git-authors": "3.1.0",
|
||||
"grunt-html": "6.0.0",
|
||||
"grunt-jscs": "2.1.0",
|
||||
"load-grunt-tasks": "3.4.0",
|
||||
"rimraf": "2.5.1",
|
||||
"testswarm": "1.1.0"
|
||||
},
|
||||
"homepage": "http://jqueryui.com",
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"main": "ui/widget.js",
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "scott.gonzalez@gmail.com",
|
||||
"name": "Scott Gonz\u00e1lez",
|
||||
"url": "http://scottgonzalez.com"
|
||||
},
|
||||
{
|
||||
"email": "joern.zaefferer@gmail.com",
|
||||
"name": "J\u00f6rn Zaefferer",
|
||||
"url": "http://bassistance.de"
|
||||
},
|
||||
{
|
||||
"email": "mike.sherov@gmail.com",
|
||||
"name": "Mike Sherov",
|
||||
"url": "http://mike.sherov.com"
|
||||
},
|
||||
{
|
||||
"email": "tj.vantoll@gmail.com",
|
||||
"name": "TJ VanToll",
|
||||
"url": "http://tjvantoll.com"
|
||||
},
|
||||
{
|
||||
"email": "info@felixnagel.com",
|
||||
"name": "Felix Nagel",
|
||||
"url": "http://www.felixnagel.com"
|
||||
},
|
||||
{
|
||||
"email": "arschmitz@gmail.com",
|
||||
"name": "Alex Schmitz",
|
||||
"url": "https://github.com/arschmitz"
|
||||
}
|
||||
],
|
||||
"name": "jquery-ui",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/jquery/jquery-ui.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt"
|
||||
},
|
||||
"title": "jQuery UI",
|
||||
"version": "1.12.1"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#tabs {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
#tabs>.dashboard-cell>.dashboard-panel {
|
||||
margin-bottom: 0px;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
#element1>.html {
|
||||
padding-left: 0px;
|
||||
/* This fixes some weirdness where the first tab doesn't look right due to the left part of the tab showing */
|
||||
padding-right: 0px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
#tab_1>.dashboard-cell>.dashboard-panel {
|
||||
border-top: 0px;
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
require(['jquery','underscore','splunkjs/mvc', 'bootstrap.tab', 'splunkjs/mvc/simplexml/ready!'],
|
||||
function($, _, mvc){
|
||||
|
||||
var tabsInitialzed = [];
|
||||
|
||||
/**
|
||||
* The below defines the tab handling logic.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This hides the content associated with the tabs.
|
||||
*
|
||||
* The normal, auto-magical Bootstrap tab processing doesn't work for us since it requires a particular
|
||||
* layout of HTML that we cannot use without converting the view entirely to simpleXML. So, we are
|
||||
* going to handle it ourselves.
|
||||
* @param {string} tabSetClass the
|
||||
*/
|
||||
var hideTabTargets = function(tabSetClass) {
|
||||
|
||||
var tabs = $('a[data-elements]');
|
||||
|
||||
// If we are only applying this to a particular set of tabs, then limit the selector accordingly
|
||||
if (typeof tabSetClass !== 'undefined' && tabSetClass) {
|
||||
tabs = $('a.' + tabSetClass + '[data-elements]');
|
||||
}
|
||||
|
||||
// Go through each toggle tab
|
||||
for (var c = 0; c < tabs.length; c++) {
|
||||
|
||||
// Hide the targets associated with the tab
|
||||
var targets = $(tabs[c]).data("elements").split(",");
|
||||
|
||||
for (var d = 0; d < targets.length; d++) {
|
||||
$('#' + targets[d], this.$el).hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Force a re-render of the panels with the given row ID.
|
||||
*
|
||||
* @param {string} row_id The ID of the row to force a rerender on
|
||||
* @param {bool} force Force the tab to re-render even if it was already rendered once (defaults to true)
|
||||
*/
|
||||
var rerenderPanels = function(row_id, force){
|
||||
|
||||
// Set a default argument for dont_rerender_until_needed
|
||||
if( typeof force === 'undefined'){
|
||||
force = true;
|
||||
}
|
||||
|
||||
// Don't do both if the panel was already rendered
|
||||
if( !force && _.contains(tabsInitialzed, row_id) ){
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the elements so that we can find the components to re-render
|
||||
var elements = $('#' + row_id + ' .dashboard-element');
|
||||
|
||||
// Iterate the list and re-render the components so that they fill the screen
|
||||
for(var d = 0; d < elements.length; d++){
|
||||
|
||||
// Determine if this is re-sizable
|
||||
if( $('#' + row_id + ' .ui-resizable').length > 0){
|
||||
|
||||
var component = mvc.Components.get(elements[d].id);
|
||||
|
||||
if(component){
|
||||
component.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remember that we initialized this tab
|
||||
tabsInitialzed.push(row_id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the selection of a partiular tab.
|
||||
*
|
||||
* @param {*} e
|
||||
*/
|
||||
var selectTab = function (e) {
|
||||
// Update which tab is considered active
|
||||
$('#tabs > li.active').removeClass("active");
|
||||
$(e.target).closest("li").addClass("active");
|
||||
|
||||
// clearTabControlTokens();
|
||||
setActiveTabToken();
|
||||
|
||||
// Stop if the tabs have no elements
|
||||
if( $(e.target).data("elements") === undefined ){
|
||||
console.warn("Yikes, the clicked tab has no elements to hide!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if the set of tabs has a restriction on the classes to manipulate
|
||||
var tabSet = null;
|
||||
|
||||
if ($(e.target).data("tab-set") !== undefined) {
|
||||
tabSet = $(e.target).data("tab-set");
|
||||
}
|
||||
|
||||
// Get the IDs that we should enable for this tab
|
||||
var toToggle = $(e.target).data("elements").split(",");
|
||||
|
||||
// Hide the tab content by default
|
||||
hideTabTargets(tabSet);
|
||||
|
||||
// Now show this tabs toggle elements
|
||||
for(var c = 0; c < toToggle.length; c++){
|
||||
|
||||
// Show the items
|
||||
$('#' + toToggle[c], this.$el).show();
|
||||
|
||||
// Re-render the panels under the item if necessary
|
||||
rerenderPanels(toToggle[c]);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The code below handles the tokens that trigger when searches are kicked off for a tab.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the tab token for a given tab name
|
||||
* @param {string} tab_name The name of the tab
|
||||
*/
|
||||
var getTabTokenForTabName = function(tab_name){
|
||||
return tab_name;
|
||||
};
|
||||
|
||||
// Get all of the possible tab control tokens
|
||||
var getTabTokens = function(){
|
||||
var tabTokens = [];
|
||||
|
||||
var tabLinks = $('#tabs > li > a');
|
||||
|
||||
for(var c = 0; c < tabLinks.length; c++){
|
||||
tabTokens.push( getTabTokenForTabName( $(tabLinks[c]).data('token') ) );
|
||||
}
|
||||
|
||||
return tabTokens;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all but the active tab control tokens
|
||||
*/
|
||||
var clearTabControlTokens = function(){
|
||||
console.info("Clearing tab control tokens");
|
||||
|
||||
//tabsInitialzed = [];
|
||||
var tabTokens = getTabTokens();
|
||||
var activeTabToken = getActiveTabToken();
|
||||
var tokens = mvc.Components.getInstance("submitted");
|
||||
|
||||
// Clear the tokens for all tabs except for the active one
|
||||
for(var c = 0; c < tabTokens.length; c++){
|
||||
|
||||
if( activeTabToken !== tabTokens[c] ){
|
||||
tokens.set(tabTokens[c], undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the tab control token for the active tab
|
||||
*/
|
||||
var getActiveTabToken = function(){
|
||||
return $('#tabs > li.active > a').data('token');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the token for the active tab
|
||||
*/
|
||||
var setActiveTabToken = function(){
|
||||
var activeTabToken = getActiveTabToken();
|
||||
var tokens = mvc.Components.getInstance("submitted");
|
||||
|
||||
if(activeTabToken){
|
||||
// Set each token if necessary
|
||||
activeTabToken.split(",").forEach(function(token){
|
||||
|
||||
// If the token wasn't set, set it so that the searches can run
|
||||
if(!tokens.toJSON()[token] || tokens.toJSON()[token] == undefined){
|
||||
tokens.set(token, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the setting of the token for the clicked tab.
|
||||
* @param {*} e
|
||||
*/
|
||||
var setTokenForTab = function(e){
|
||||
|
||||
// Get the token for the tab
|
||||
var tabToken = getTabTokenForTabName($(e.target).data('token'));
|
||||
|
||||
// Set the token
|
||||
var tokens = mvc.Components.getInstance("submitted");
|
||||
tokens.set(tabToken, '');
|
||||
|
||||
console.info("Set the token for the active tab (" + tabToken + ")");
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform the initial setup for making the tabs work.
|
||||
*/
|
||||
var firstTimeTabSetup = function() {
|
||||
$('a.toggle-tab').on('shown', setTokenForTab);
|
||||
|
||||
// Wire up the function to show the appropriate tab
|
||||
$('a.toggle-tab').on('click shown', selectTab);
|
||||
|
||||
// Show the first tab in each tab set
|
||||
$.each($('.nav-tabs'), function(index, value) {
|
||||
$('.toggle-tab', value).first().trigger('shown');
|
||||
});
|
||||
|
||||
// Make the tabs into tabs
|
||||
$('#tabs', this.$el).tab();
|
||||
|
||||
// Wire up the tab control tokenization
|
||||
var submit = mvc.Components.get("submit");
|
||||
|
||||
if(submit){
|
||||
submit.on("submit", function() {
|
||||
clearTabControlTokens();
|
||||
});
|
||||
}
|
||||
|
||||
// Set the token for the selected tab
|
||||
setActiveTabToken();
|
||||
};
|
||||
|
||||
firstTimeTabSetup();
|
||||
});
|
||||
@@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
#
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
#
|
||||
# Code modified from DNSTwist (https://github.com/elceef/dnstwist)
|
||||
# Thanks elceef!
|
||||
#
|
||||
# Changes made:
|
||||
# Just kept the DomainFuzz class and passing the domain to the fuzzer. Then added
|
||||
# the Splunk specific code around it
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
import re
|
||||
import csv
|
||||
import time
|
||||
import os
|
||||
|
||||
from splunklib.searchcommands import dispatch, GeneratingCommand, \
|
||||
Configuration, Option, Boolean
|
||||
from splunk.clilib.bundle_paths import make_splunkhome_path
|
||||
|
||||
|
||||
class DomainFuzz(object):
|
||||
|
||||
def __init__(self, domain):
|
||||
self.domain, self.tld = self.__domain_tld(domain)
|
||||
self.domains = []
|
||||
self.qwerty = {
|
||||
'1': '2q', '2': '3wq1', '3': '4ew2', '4': '5re3',
|
||||
'5': '6tr4', '6': '7yt5', '7': '8uy6', '8': '9iu7',
|
||||
'9': '0oi8', '0': 'po9', 'q': '12wa', 'w': '3esaq2',
|
||||
'e': '4rdsw3', 'r': '5tfde4', 't': '6ygfr5', 'y': '7uhgt6',
|
||||
'u': '8ijhy7', 'i': '9okju8', 'o': '0plki9', 'p': 'lo0',
|
||||
'a': 'qwsz', 's': 'edxzaw', 'd': 'rfcxse', 'f': 'tgvcdr',
|
||||
'g': 'yhbvft', 'h': 'ujnbgy', 'j': 'ikmnhu', 'k': 'olmji',
|
||||
'l': 'kop', 'z': 'asx', 'x': 'zsdc', 'c': 'xdfv',
|
||||
'v': 'cfgb', 'b': 'vghn', 'n': 'bhjm', 'm': 'njk'
|
||||
}
|
||||
self.qwertz = {
|
||||
'1': '2q', '2': '3wq1', '3': '4ew2', '4': '5re3',
|
||||
'5': '6tr4', '6': '7zt5', '7': '8uz6', '8': '9iu7',
|
||||
'9': '0oi8', '0': 'po9', 'q': '12wa', 'w': '3esaq2',
|
||||
'e': '4rdsw3', 'r': '5tfde4', 't': '6zgfr5',
|
||||
'z': '7uhgt6', 'u': '8ijhz7', 'i': '9okju8',
|
||||
'o': '0plki9', 'p': 'lo0', 'a': 'qwsy', 's': 'edxyaw',
|
||||
'd': 'rfcxse', 'f': 'tgvcdr', 'g': 'zhbvft',
|
||||
'h': 'ujnbgz', 'j': 'ikmnhu', 'k': 'olmji', 'l': 'kop',
|
||||
'y': 'asx', 'x': 'ysdc', 'c': 'xdfv', 'v': 'cfgb',
|
||||
'b': 'vghn', 'n': 'bhjm', 'm': 'njk'
|
||||
}
|
||||
self.azerty = {
|
||||
'1': '2a', '2': '3za1', '3': '4ez2', '4': '5re3',
|
||||
'5': '6tr4', '6': '7yt5', '7': '8uy6', '8': '9iu7',
|
||||
'9': '0oi8', '0': 'po9', 'a': '2zq1', 'z': '3esqa2',
|
||||
'e': '4rdsz3', 'r': '5tfde4', 't': '6ygfr5',
|
||||
'y': '7uhgt6', 'u': '8ijhy7', 'i': '9okju8',
|
||||
'o': '0plki9', 'p': 'lo0m', 'q': 'zswa', 's': 'edxwqz',
|
||||
'd': 'rfcxse', 'f': 'tgvcdr', 'g': 'yhbvft',
|
||||
'h': 'ujnbgy', 'j': 'iknhu', 'k': 'olji', 'l': 'kopm',
|
||||
'm': 'lp', 'w': 'sxq', 'x': 'zsdc', 'c': 'xdfv',
|
||||
'v': 'cfgb', 'b': 'vghn', 'n': 'bhj'
|
||||
}
|
||||
self.keyboards = [self.qwerty, self.qwertz, self.azerty]
|
||||
|
||||
def __domain_tld(self, domain):
|
||||
domain = domain.rsplit('.', 2)
|
||||
|
||||
if len(domain) == 2:
|
||||
return domain[0], domain[1]
|
||||
|
||||
return domain[0] + '.' + domain[1], domain[2]
|
||||
|
||||
def __validate_domain(self, domain):
|
||||
if len(domain) == len(domain.encode('idna')) and domain != domain.encode('idna'):
|
||||
return False
|
||||
allowed = re.compile(b'(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\\.)+[a-zA-Z]{2,63}\\.?$)', re.IGNORECASE)
|
||||
return allowed.match(domain.encode('idna'))
|
||||
|
||||
def __filter_domains(self):
|
||||
seen = set()
|
||||
filtered = []
|
||||
|
||||
for d in self.domains:
|
||||
# if not self.__validate_domain(d['domain-name']):
|
||||
# p_err("debug: invalid domain %s\n" % d['domain-name'])
|
||||
try:
|
||||
if self.__validate_domain(d['domain-name']) and d['domain-name'] not in seen:
|
||||
seen.add(d['domain-name'])
|
||||
filtered.append(d)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
self.domains = filtered
|
||||
|
||||
def __bitsquatting(self):
|
||||
result = []
|
||||
masks = [1, 2, 4, 8, 16, 32, 64, 128]
|
||||
for i in range(0, len(self.domain)):
|
||||
c = self.domain[i]
|
||||
for j in range(0, len(masks)):
|
||||
b = chr(ord(c) ^ masks[j])
|
||||
o = ord(b)
|
||||
if (o >= 48 and o <= 57) or (o >= 97 and o <= 122) or o == 45:
|
||||
result.append(self.domain[:i] + b + self.domain[i+1:])
|
||||
|
||||
return result
|
||||
|
||||
def __homoglyph(self):
|
||||
glyphs = {
|
||||
'a': [u'à', u'á', u'â', u'ã', u'ä', u'å', u'ɑ', u'а', u'ạ', u'ǎ', u'ă', u'ȧ', u'ӓ'],
|
||||
'b': ['d', 'lb', 'ib', u'ʙ', u'Ь', u'b̔', u'ɓ', u'Б'],
|
||||
'c': [u'ϲ', u'с', u'ƈ', u'ċ', u'ć', u'ç'],
|
||||
'd': ['b', 'cl', 'dl', 'di', u'ԁ', u'ժ', u'ɗ', u'đ'],
|
||||
'e': [u'é', u'ê', u'ë', u'ē', u'ĕ', u'ě', u'ė', u'е', u'ẹ', u'ę', u'є', u'ϵ', u'ҽ'],
|
||||
'f': [u'Ϝ', u'ƒ', u'Ғ'],
|
||||
'g': ['q', u'ɢ', u'ɡ', u'Ԍ', u'Ԍ', u'ġ', u'ğ', u'ց', u'ǵ', u'ģ'],
|
||||
'h': ['lh', 'ih', u'һ', u'հ', u'Ꮒ', u'н'],
|
||||
'i': ['1', 'l', u'Ꭵ', u'í', u'ï', u'ı', u'ɩ', u'ι', u'ꙇ', u'ǐ', u'ĭ'],
|
||||
'j': [u'ј', u'ʝ', u'ϳ', u'ɉ'],
|
||||
'k': ['lk', 'ik', 'lc', u'κ', u'ⲕ', u'κ'],
|
||||
'l': ['1', 'i', u'ɫ', u'ł'],
|
||||
'm': ['n', 'nn', 'rn', 'rr', u'ṃ', u'ᴍ', u'м', u'ɱ'],
|
||||
'n': ['m', 'r', u'ń'],
|
||||
'o': ['0', u'Ο', u'ο', u'О', u'о', u'Օ', u'ȯ', u'ọ', u'ỏ', u'ơ', u'ó', u'ö', u'ӧ'],
|
||||
'p': [u'ρ', u'р', u'ƿ', u'Ϸ', u'Þ'],
|
||||
'q': ['g', u'զ', u'ԛ', u'գ', u'ʠ'],
|
||||
'r': [u'ʀ', u'Г', u'ᴦ', u'ɼ', u'ɽ'],
|
||||
's': [u'Ⴝ', u'Ꮪ', u'ʂ', u'ś', u'ѕ'],
|
||||
't': [u'τ', u'т', u'ţ'],
|
||||
'u': [u'μ', u'υ', u'Ս', u'ս', u'ц', u'ᴜ', u'ǔ', u'ŭ'],
|
||||
'v': [u'ѵ', u'ν', u'v̇'],
|
||||
'w': ['vv', u'ѡ', u'ա', u'ԝ'],
|
||||
'x': [u'х', u'ҳ', u'ẋ'],
|
||||
'y': [u'ʏ', u'γ', u'у', u'Ү', u'ý'],
|
||||
'z': [u'ʐ', u'ż', u'ź', u'ʐ', u'ᴢ']
|
||||
}
|
||||
|
||||
result = []
|
||||
|
||||
for ws in range(0, len(self.domain)):
|
||||
for i in range(0, (len(self.domain)-ws)+1):
|
||||
win = self.domain[i:i+ws]
|
||||
|
||||
j = 0
|
||||
while j < ws:
|
||||
c = win[j]
|
||||
if c in glyphs:
|
||||
win_copy = win
|
||||
for g in glyphs[c]:
|
||||
win = win.replace(c, g)
|
||||
result.append(self.domain[:i] + win + self.domain[i+ws:])
|
||||
win = win_copy
|
||||
j += 1
|
||||
|
||||
return list(set(result))
|
||||
|
||||
def __hyphenation(self):
|
||||
result = []
|
||||
|
||||
for i in range(1, len(self.domain)):
|
||||
result.append(self.domain[:i] + '-' + self.domain[i:])
|
||||
|
||||
return result
|
||||
|
||||
def __insertion(self):
|
||||
result = []
|
||||
|
||||
for i in range(1, len(self.domain)-1):
|
||||
for keys in self.keyboards:
|
||||
if self.domain[i] in keys:
|
||||
for c in keys[self.domain[i]]:
|
||||
result.append(self.domain[:i] + c + self.domain[i] + self.domain[i+1:])
|
||||
result.append(self.domain[:i] + self.domain[i] + c + self.domain[i+1:])
|
||||
|
||||
return list(set(result))
|
||||
|
||||
def __omission(self):
|
||||
result = []
|
||||
|
||||
for i in range(0, len(self.domain)):
|
||||
result.append(self.domain[:i] + self.domain[i+1:])
|
||||
|
||||
n = re.sub(r'(.)\1+', r'\1', self.domain)
|
||||
|
||||
if n not in result and n != self.domain:
|
||||
result.append(n)
|
||||
|
||||
return list(set(result))
|
||||
|
||||
def __repetition(self):
|
||||
result = []
|
||||
|
||||
for i in range(0, len(self.domain)):
|
||||
if self.domain[i].isalpha():
|
||||
result.append(self.domain[:i] + self.domain[i] + self.domain[i] + self.domain[i+1:])
|
||||
|
||||
return list(set(result))
|
||||
|
||||
def __replacement(self):
|
||||
result = []
|
||||
|
||||
for i in range(0, len(self.domain)):
|
||||
for keys in self.keyboards:
|
||||
if self.domain[i] in keys:
|
||||
for c in keys[self.domain[i]]:
|
||||
result.append(self.domain[:i] + c + self.domain[i+1:])
|
||||
|
||||
return list(set(result))
|
||||
|
||||
def __subdomain(self):
|
||||
result = []
|
||||
|
||||
for i in range(1, len(self.domain)):
|
||||
if self.domain[i] not in ['-', '.'] and self.domain[i-1] not in ['-', '.']:
|
||||
result.append(self.domain[:i] + '.' + self.domain[i:])
|
||||
|
||||
return result
|
||||
|
||||
def __transposition(self):
|
||||
result = []
|
||||
|
||||
for i in range(0, len(self.domain)-1):
|
||||
if self.domain[i+1] != self.domain[i]:
|
||||
result.append(self.domain[:i] + self.domain[i+1] + self.domain[i] + self.domain[i+2:])
|
||||
|
||||
return result
|
||||
|
||||
def __vowel_swap(self):
|
||||
vowels = 'aeiou'
|
||||
result = []
|
||||
|
||||
for i in range(0, len(self.domain)):
|
||||
for vowel in vowels:
|
||||
if self.domain[i] in vowels:
|
||||
result.append(self.domain[:i] + vowel + self.domain[i+1:])
|
||||
|
||||
return list(set(result))
|
||||
|
||||
def __addition(self):
|
||||
result = []
|
||||
|
||||
for i in range(97, 123):
|
||||
result.append(self.domain + chr(i))
|
||||
|
||||
return result
|
||||
|
||||
def generate(self):
|
||||
self.domains.append({'fuzzer': 'Original*', 'domain-name': self.domain + '.' + self.tld})
|
||||
|
||||
for domain in self.__addition():
|
||||
self.domains.append({'fuzzer': 'Addition', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__bitsquatting():
|
||||
self.domains.append({'fuzzer': 'Bitsquatting', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__homoglyph():
|
||||
self.domains.append({'fuzzer': 'Homoglyph', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__hyphenation():
|
||||
self.domains.append({'fuzzer': 'Hyphenation', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__insertion():
|
||||
self.domains.append({'fuzzer': 'Insertion', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__omission():
|
||||
self.domains.append({'fuzzer': 'Omission', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__repetition():
|
||||
self.domains.append({'fuzzer': 'Repetition', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__replacement():
|
||||
self.domains.append({'fuzzer': 'Replacement', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__subdomain():
|
||||
self.domains.append({'fuzzer': 'Subdomain', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__transposition():
|
||||
self.domains.append({'fuzzer': 'Transposition', 'domain-name': domain + '.' + self.tld})
|
||||
for domain in self.__vowel_swap():
|
||||
self.domains.append({'fuzzer': 'Vowel-swap', 'domain-name': domain + '.' + self.tld})
|
||||
|
||||
if not self.domain.startswith('www.'):
|
||||
self.domains.append({'fuzzer': 'Various', 'domain-name': 'ww' + self.domain + '.' + self.tld})
|
||||
self.domains.append({'fuzzer': 'Various', 'domain-name': 'www' + self.domain + '.' + self.tld})
|
||||
self.domains.append({'fuzzer': 'Various', 'domain-name': 'www-' + self.domain + '.' + self.tld})
|
||||
if '.' in self.tld:
|
||||
self.domains.append({'fuzzer': 'Various', 'domain-name': self.domain + '.' + self.tld.split('.')[-1]})
|
||||
self.domains.append({'fuzzer': 'Various', 'domain-name': self.domain + self.tld})
|
||||
if '.' not in self.tld:
|
||||
self.domains.append({'fuzzer': 'Various', 'domain-name': self.domain + self.tld + '.' + self.tld})
|
||||
if self.tld != 'com' and '.' not in self.tld:
|
||||
self.domains.append({'fuzzer': 'Various', 'domain-name': self.domain + '-' + self.tld + '.com'})
|
||||
|
||||
self.__filter_domains()
|
||||
|
||||
|
||||
@Configuration(distributed=True)
|
||||
class DnsTwistCommand(GeneratingCommand):
|
||||
|
||||
domainlist_file_name = Option(doc='''
|
||||
**Syntax:** **domainlist=***<path>*
|
||||
**Description:** CSV file from which repeated random samples will be drawn
|
||||
''', name='domainlist', require=False)
|
||||
|
||||
populate_from_cim = Option(doc='''
|
||||
**Syntax: populate_cim=<bool>
|
||||
**Description:** When `true`, populates Splunk_SA_CIM lookups cim_corporate_email_domains.csv
|
||||
and cim_corporate_web_domains.csv with dnstwisted domains. Defaults to `false`.
|
||||
''', name='populate_from_cim', default=False, validate=Boolean())
|
||||
|
||||
domain = Option(doc='''
|
||||
**Syntax:** **domain=***<domain name>*
|
||||
**Description:** Domain to DNS generated twisted entries for.
|
||||
''', name='domain', require=False, default='')
|
||||
|
||||
def generate(self):
|
||||
event_count = 0
|
||||
csv_file_names = []
|
||||
|
||||
if self.populate_from_cim:
|
||||
csv_file_names.append(make_splunkhome_path([
|
||||
'etc',
|
||||
'apps',
|
||||
'Splunk_SA_CIM',
|
||||
'lookups',
|
||||
'cim_corporate_email_domains.csv']))
|
||||
csv_file_names.append(make_splunkhome_path([
|
||||
'etc',
|
||||
'apps',
|
||||
'Splunk_SA_CIM',
|
||||
'lookups',
|
||||
'cim_corporate_web_domains.csv']))
|
||||
|
||||
# Make sure we just get the base file name from file. In case there was some directory traversal going on.
|
||||
if self.domainlist_file_name:
|
||||
sanitized_file_name = os.path.basename(self.domainlist_file_name)
|
||||
lookup_path = make_splunkhome_path(['etc', 'apps', 'DA-ESS-ContentUpdate', 'lookups', sanitized_file_name])
|
||||
|
||||
# Make sure there really isn't any directory traversal going on.
|
||||
valid_path = True
|
||||
if "../" in lookup_path:
|
||||
valid_path = False
|
||||
|
||||
# Make sure the path that is created by adding the file name to the path is the same as the
|
||||
# absolute path
|
||||
if lookup_path != os.path.abspath(lookup_path):
|
||||
valid_path = False
|
||||
|
||||
if valid_path:
|
||||
csv_file_names.append(lookup_path)
|
||||
|
||||
domains_to_twist = []
|
||||
|
||||
for csv_file_name in csv_file_names:
|
||||
if os.path.exists(csv_file_name):
|
||||
# this is nasty but works .. please forgive me
|
||||
if sys.version_info >= (3, 0):
|
||||
csv_file = open(csv_file_name, "r", newline='')
|
||||
else:
|
||||
csv_file = open(csv_file_name, "r")
|
||||
for input_domain in csv.DictReader(csv_file):
|
||||
if input_domain['domain'] not in domains_to_twist:
|
||||
domains_to_twist.append(input_domain['domain'])
|
||||
|
||||
# if a single domain is passed lets just calculate that
|
||||
if self.domain != '':
|
||||
domains_to_twist = []
|
||||
domains_to_twist.append(self.domain)
|
||||
|
||||
for domain_to_twist in domains_to_twist:
|
||||
domain_to_twist = domain_to_twist.lstrip('*')
|
||||
dfuzz = DomainFuzz(domain_to_twist)
|
||||
dfuzz.generate()
|
||||
domains = dfuzz.domains
|
||||
for domain in domains:
|
||||
# We don't want to keep the original domain
|
||||
if domain['domain-name'] in domain_to_twist:
|
||||
continue
|
||||
event_count += 1
|
||||
yield {
|
||||
'_time': time.time(),
|
||||
'event_no': event_count,
|
||||
'_raw': domain['domain-name'],
|
||||
'domain': '*'+domain['domain-name']+'*',
|
||||
'original_domain': domain_to_twist
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super(DnsTwistCommand, self).__init__()
|
||||
|
||||
|
||||
dispatch(DnsTwistCommand, sys.argv, sys.stdin, sys.stdout, __name__)
|
||||
@@ -0,0 +1 @@
|
||||
# dropped AR action support due to python 3 dependency, we leverage playbooks in stories as an alternative.
|
||||
@@ -0,0 +1 @@
|
||||
# dropped AR action support due to python 3 dependency, we leverage playbooks in stories as an alternative.
|
||||
@@ -0,0 +1 @@
|
||||
# runstory was deprecated, its functionality was moved to: https://github.com/splunk/analytic_story_execution
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Python library for Splunk."""
|
||||
|
||||
__version_info__ = (1, 6, 2)
|
||||
__version__ = ".".join(map(str, __version_info__))
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""The **splunklib.data** module reads the responses from splunkd in Atom Feed
|
||||
format, which is the format used by most of the REST API.
|
||||
"""
|
||||
|
||||
from xml.etree.ElementTree import XML
|
||||
|
||||
__all__ = ["load"]
|
||||
|
||||
# LNAME refers to element names without namespaces; XNAME is the same
|
||||
# name, but with an XML namespace.
|
||||
LNAME_DICT = "dict"
|
||||
LNAME_ITEM = "item"
|
||||
LNAME_KEY = "key"
|
||||
LNAME_LIST = "list"
|
||||
|
||||
XNAMEF_REST = "{http://dev.splunk.com/ns/rest}%s"
|
||||
XNAME_DICT = XNAMEF_REST % LNAME_DICT
|
||||
XNAME_ITEM = XNAMEF_REST % LNAME_ITEM
|
||||
XNAME_KEY = XNAMEF_REST % LNAME_KEY
|
||||
XNAME_LIST = XNAMEF_REST % LNAME_LIST
|
||||
|
||||
# Some responses don't use namespaces (eg: search/parse) so we look for
|
||||
# both the extended and local versions of the following names.
|
||||
|
||||
def isdict(name):
|
||||
return name == XNAME_DICT or name == LNAME_DICT
|
||||
|
||||
def isitem(name):
|
||||
return name == XNAME_ITEM or name == LNAME_ITEM
|
||||
|
||||
def iskey(name):
|
||||
return name == XNAME_KEY or name == LNAME_KEY
|
||||
|
||||
def islist(name):
|
||||
return name == XNAME_LIST or name == LNAME_LIST
|
||||
|
||||
def hasattrs(element):
|
||||
return len(element.attrib) > 0
|
||||
|
||||
def localname(xname):
|
||||
rcurly = xname.find('}')
|
||||
return xname if rcurly == -1 else xname[rcurly+1:]
|
||||
|
||||
def load(text, match=None):
|
||||
"""This function reads a string that contains the XML of an Atom Feed, then
|
||||
returns the
|
||||
data in a native Python structure (a ``dict`` or ``list``). If you also
|
||||
provide a tag name or path to match, only the matching sub-elements are
|
||||
loaded.
|
||||
|
||||
:param text: The XML text to load.
|
||||
:type text: ``string``
|
||||
:param match: A tag name or path to match (optional).
|
||||
:type match: ``string``
|
||||
"""
|
||||
if text is None: return None
|
||||
text = text.strip()
|
||||
if len(text) == 0: return None
|
||||
nametable = {
|
||||
'namespaces': [],
|
||||
'names': {}
|
||||
}
|
||||
root = XML(text)
|
||||
items = [root] if match is None else root.findall(match)
|
||||
count = len(items)
|
||||
if count == 0:
|
||||
return None
|
||||
elif count == 1:
|
||||
return load_root(items[0], nametable)
|
||||
else:
|
||||
return [load_root(item, nametable) for item in items]
|
||||
|
||||
# Load the attributes of the given element.
|
||||
def load_attrs(element):
|
||||
if not hasattrs(element): return None
|
||||
attrs = record()
|
||||
for key, value in element.attrib.iteritems():
|
||||
attrs[key] = value
|
||||
return attrs
|
||||
|
||||
# Parse a <dict> element and return a Python dict
|
||||
def load_dict(element, nametable = None):
|
||||
value = record()
|
||||
children = list(element)
|
||||
for child in children:
|
||||
assert iskey(child.tag)
|
||||
name = child.attrib["name"]
|
||||
value[name] = load_value(child, nametable)
|
||||
return value
|
||||
|
||||
# Loads the given elements attrs & value into single merged dict.
|
||||
def load_elem(element, nametable=None):
|
||||
name = localname(element.tag)
|
||||
attrs = load_attrs(element)
|
||||
value = load_value(element, nametable)
|
||||
if attrs is None: return name, value
|
||||
if value is None: return name, attrs
|
||||
# If value is simple, merge into attrs dict using special key
|
||||
if isinstance(value, str):
|
||||
attrs["$text"] = value
|
||||
return name, attrs
|
||||
# Both attrs & value are complex, so merge the two dicts, resolving collisions.
|
||||
collision_keys = []
|
||||
for key, val in attrs.iteritems():
|
||||
if key in value and key in collision_keys:
|
||||
value[key].append(val)
|
||||
elif key in value and key not in collision_keys:
|
||||
value[key] = [value[key], val]
|
||||
collision_keys.append(key)
|
||||
else:
|
||||
value[key] = val
|
||||
return name, value
|
||||
|
||||
# Parse a <list> element and return a Python list
|
||||
def load_list(element, nametable=None):
|
||||
assert islist(element.tag)
|
||||
value = []
|
||||
children = list(element)
|
||||
for child in children:
|
||||
assert isitem(child.tag)
|
||||
value.append(load_value(child, nametable))
|
||||
return value
|
||||
|
||||
# Load the given root element.
|
||||
def load_root(element, nametable=None):
|
||||
tag = element.tag
|
||||
if isdict(tag): return load_dict(element, nametable)
|
||||
if islist(tag): return load_list(element, nametable)
|
||||
k, v = load_elem(element, nametable)
|
||||
return Record.fromkv(k, v)
|
||||
|
||||
# Load the children of the given element.
|
||||
def load_value(element, nametable=None):
|
||||
children = list(element)
|
||||
count = len(children)
|
||||
|
||||
# No children, assume a simple text value
|
||||
if count == 0:
|
||||
text = element.text
|
||||
if text is None:
|
||||
return None
|
||||
text = text.strip()
|
||||
if len(text) == 0:
|
||||
return None
|
||||
return text
|
||||
|
||||
# Look for the special case of a single well-known structure
|
||||
if count == 1:
|
||||
child = children[0]
|
||||
tag = child.tag
|
||||
if isdict(tag): return load_dict(child, nametable)
|
||||
if islist(tag): return load_list(child, nametable)
|
||||
|
||||
value = record()
|
||||
for child in children:
|
||||
name, item = load_elem(child, nametable)
|
||||
# If we have seen this name before, promote the value to a list
|
||||
if value.has_key(name):
|
||||
current = value[name]
|
||||
if not isinstance(current, list):
|
||||
value[name] = [current]
|
||||
value[name].append(item)
|
||||
else:
|
||||
value[name] = item
|
||||
|
||||
return value
|
||||
|
||||
# A generic utility that enables "dot" access to dicts
|
||||
class Record(dict):
|
||||
"""This generic utility class enables dot access to members of a Python
|
||||
dictionary.
|
||||
|
||||
Any key that is also a valid Python identifier can be retrieved as a field.
|
||||
So, for an instance of ``Record`` called ``r``, ``r.key`` is equivalent to
|
||||
``r['key']``. A key such as ``invalid-key`` or ``invalid.key`` cannot be
|
||||
retrieved as a field, because ``-`` and ``.`` are not allowed in
|
||||
identifiers.
|
||||
|
||||
Keys of the form ``a.b.c`` are very natural to write in Python as fields. If
|
||||
a group of keys shares a prefix ending in ``.``, you can retrieve keys as a
|
||||
nested dictionary by calling only the prefix. For example, if ``r`` contains
|
||||
keys ``'foo'``, ``'bar.baz'``, and ``'bar.qux'``, ``r.bar`` returns a record
|
||||
with the keys ``baz`` and ``qux``. If a key contains multiple ``.``, each
|
||||
one is placed into a nested dictionary, so you can write ``r.bar.qux`` or
|
||||
``r['bar.qux']`` interchangeably.
|
||||
"""
|
||||
sep = '.'
|
||||
|
||||
def __call__(self, *args):
|
||||
if len(args) == 0: return self
|
||||
return Record((key, self[key]) for key in args)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __delattr__(self, name):
|
||||
del self[name]
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self[name] = value
|
||||
|
||||
@staticmethod
|
||||
def fromkv(k, v):
|
||||
result = record()
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self:
|
||||
return dict.__getitem__(self, key)
|
||||
key += self.sep
|
||||
result = record()
|
||||
for k,v in self.iteritems():
|
||||
if not k.startswith(key):
|
||||
continue
|
||||
suffix = k[len(key):]
|
||||
if '.' in suffix:
|
||||
ks = suffix.split(self.sep)
|
||||
z = result
|
||||
for x in ks[:-1]:
|
||||
if x not in z:
|
||||
z[x] = record()
|
||||
z = z[x]
|
||||
z[ks[-1]] = v
|
||||
else:
|
||||
result[suffix] = v
|
||||
if len(result) == 0:
|
||||
raise KeyError("No key or prefix: %s" % key)
|
||||
return result
|
||||
|
||||
|
||||
def record(value=None):
|
||||
"""This function returns a :class:`Record` instance constructed with an
|
||||
initial value that you provide.
|
||||
|
||||
:param `value`: An initial record value.
|
||||
:type `value`: ``dict``
|
||||
"""
|
||||
if value is None: value = {}
|
||||
return Record(value)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""The following imports allow these classes to be imported via
|
||||
the splunklib.modularinput package like so:
|
||||
|
||||
from splunklib.modularinput import *
|
||||
"""
|
||||
from .argument import Argument
|
||||
from .event import Event
|
||||
from .event_writer import EventWriter
|
||||
from .input_definition import InputDefinition
|
||||
from .scheme import Scheme
|
||||
from .script import Script
|
||||
from .validation_definition import ValidationDefinition
|
||||
@@ -0,0 +1,102 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
try:
|
||||
import xml.etree.ElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.cElementTree as ET
|
||||
|
||||
class Argument(object):
|
||||
"""Class representing an argument to a modular input kind.
|
||||
|
||||
``Argument`` is meant to be used with ``Scheme`` to generate an XML
|
||||
definition of the modular input kind that Splunk understands.
|
||||
|
||||
``name`` is the only required parameter for the constructor.
|
||||
|
||||
**Example with least parameters**::
|
||||
|
||||
arg1 = Argument(name="arg1")
|
||||
|
||||
**Example with all parameters**::
|
||||
|
||||
arg2 = Argument(
|
||||
name="arg2",
|
||||
description="This is an argument with lots of parameters",
|
||||
validation="is_pos_int('some_name')",
|
||||
data_type=Argument.data_type_number,
|
||||
required_on_edit=True,
|
||||
required_on_create=True
|
||||
)
|
||||
"""
|
||||
|
||||
# Constant values, do not change.
|
||||
# These should be used for setting the value of an Argument object's data_type field.
|
||||
data_type_boolean = "BOOLEAN"
|
||||
data_type_number = "NUMBER"
|
||||
data_type_string = "STRING"
|
||||
|
||||
def __init__(self, name, description=None, validation=None,
|
||||
data_type=data_type_string, required_on_edit=False, required_on_create=False, title=None):
|
||||
"""
|
||||
:param name: ``string``, identifier for this argument in Splunk.
|
||||
:param description: ``string``, human-readable description of the argument.
|
||||
:param validation: ``string`` specifying how the argument should be validated, if using internal validation.
|
||||
If using external validation, this will be ignored.
|
||||
:param data_type: ``string``, data type of this field; use the class constants.
|
||||
"data_type_boolean", "data_type_number", or "data_type_string".
|
||||
:param required_on_edit: ``Boolean``, whether this arg is required when editing an existing modular input of this kind.
|
||||
:param required_on_create: ``Boolean``, whether this arg is required when creating a modular input of this kind.
|
||||
:param title: ``String``, a human-readable title for the argument.
|
||||
"""
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.validation = validation
|
||||
self.data_type = data_type
|
||||
self.required_on_edit = required_on_edit
|
||||
self.required_on_create = required_on_create
|
||||
self.title = title
|
||||
|
||||
def add_to_document(self, parent):
|
||||
"""Adds an ``Argument`` object to this ElementTree document.
|
||||
|
||||
Adds an <arg> subelement to the parent element, typically <args>
|
||||
and sets up its subelements with their respective text.
|
||||
|
||||
:param parent: An ``ET.Element`` to be the parent of a new <arg> subelement
|
||||
:returns: An ``ET.Element`` object representing this argument.
|
||||
"""
|
||||
arg = ET.SubElement(parent, "arg")
|
||||
arg.set("name", self.name)
|
||||
|
||||
if self.title is not None:
|
||||
ET.SubElement(arg, "title").text = self.title
|
||||
|
||||
if self.description is not None:
|
||||
ET.SubElement(arg, "description").text = self.description
|
||||
|
||||
if self.validation is not None:
|
||||
ET.SubElement(arg, "validation").text = self.validation
|
||||
|
||||
# add all other subelements to this Argument, represented by (tag, text)
|
||||
subelements = [
|
||||
("data_type", self.data_type),
|
||||
("required_on_edit", self.required_on_edit),
|
||||
("required_on_create", self.required_on_create)
|
||||
]
|
||||
|
||||
for name, value in subelements:
|
||||
ET.SubElement(arg, name).text = str(value).lower()
|
||||
|
||||
return arg
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError as ie:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class Event(object):
|
||||
"""Represents an event or fragment of an event to be written by this modular input to Splunk.
|
||||
|
||||
To write an input to a stream, call the ``write_to`` function, passing in a stream.
|
||||
"""
|
||||
def __init__(self, data=None, stanza=None, time=None, host=None, index=None, source=None,
|
||||
sourcetype=None, done=True, unbroken=True):
|
||||
"""There are no required parameters for constructing an Event
|
||||
|
||||
**Example with minimal configuration**::
|
||||
|
||||
my_event = Event(
|
||||
data="This is a test of my new event.",
|
||||
stanza="myStanzaName",
|
||||
time="%.3f" % 1372187084.000
|
||||
)
|
||||
|
||||
**Example with full configuration**::
|
||||
|
||||
excellent_event = Event(
|
||||
data="This is a test of my excellent event.",
|
||||
stanza="excellenceOnly",
|
||||
time="%.3f" % 1372274622.493,
|
||||
host="localhost",
|
||||
index="main",
|
||||
source="Splunk",
|
||||
sourcetype="misc",
|
||||
done=True,
|
||||
unbroken=True
|
||||
)
|
||||
|
||||
:param data: ``string``, the event's text.
|
||||
:param stanza: ``string``, name of the input this event should be sent to.
|
||||
:param time: ``float``, time in seconds, including up to 3 decimal places to represent milliseconds.
|
||||
:param host: ``string``, the event's host, ex: localhost.
|
||||
:param index: ``string``, the index this event is specified to write to, or None if default index.
|
||||
:param source: ``string``, the source of this event, or None to have Splunk guess.
|
||||
:param sourcetype: ``string``, source type currently set on this event, or None to have Splunk guess.
|
||||
:param done: ``boolean``, is this a complete ``Event``? False if an ``Event`` fragment.
|
||||
:param unbroken: ``boolean``, Is this event completely encapsulated in this ``Event`` object?
|
||||
"""
|
||||
self.data = data
|
||||
self.done = done
|
||||
self.host = host
|
||||
self.index = index
|
||||
self.source = source
|
||||
self.sourceType = sourcetype
|
||||
self.stanza = stanza
|
||||
self.time = time
|
||||
self.unbroken = unbroken
|
||||
|
||||
def write_to(self, stream):
|
||||
"""Write an XML representation of self, an ``Event`` object, to the given stream.
|
||||
|
||||
The ``Event`` object will only be written if its data field is defined,
|
||||
otherwise a ``ValueError`` is raised.
|
||||
|
||||
:param stream: stream to write XML to.
|
||||
"""
|
||||
if self.data is None:
|
||||
raise ValueError("Events must have at least the data field set to be written to XML.")
|
||||
|
||||
event = ET.Element("event")
|
||||
if self.stanza is not None:
|
||||
event.set("stanza", self.stanza)
|
||||
event.set("unbroken", str(int(self.unbroken)))
|
||||
|
||||
# if a time isn't set, let Splunk guess by not creating a <time> element
|
||||
if self.time is not None:
|
||||
ET.SubElement(event, "time").text = str(self.time)
|
||||
|
||||
# add all other subelements to this Event, represented by (tag, text)
|
||||
subelements = [
|
||||
("source", self.source),
|
||||
("sourcetype", self.sourceType),
|
||||
("index", self.index),
|
||||
("host", self.host),
|
||||
("data", self.data)
|
||||
]
|
||||
for node, value in subelements:
|
||||
if value is not None:
|
||||
ET.SubElement(event, node).text = value
|
||||
|
||||
if self.done:
|
||||
ET.SubElement(event, "done")
|
||||
|
||||
stream.write(ET.tostring(event))
|
||||
stream.flush()
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import sys
|
||||
|
||||
from .event import ET
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
class EventWriter(object):
|
||||
"""``EventWriter`` writes events and error messages to Splunk from a modular input.
|
||||
|
||||
Its two important methods are ``writeEvent``, which takes an ``Event`` object,
|
||||
and ``log``, which takes a severity and an error message.
|
||||
"""
|
||||
|
||||
# Severities that Splunk understands for log messages from modular inputs.
|
||||
# Do not change these
|
||||
DEBUG = "DEBUG"
|
||||
INFO = "INFO"
|
||||
WARN = "WARN"
|
||||
ERROR = "ERROR"
|
||||
FATAL = "FATAL"
|
||||
|
||||
def __init__(self, output = sys.stdout, error = sys.stderr):
|
||||
"""
|
||||
:param output: Where to write the output; defaults to sys.stdout.
|
||||
:param error: Where to write any errors; defaults to sys.stderr.
|
||||
"""
|
||||
self._out = output
|
||||
self._err = error
|
||||
|
||||
# has the opening <stream> tag been written yet?
|
||||
self.header_written = False
|
||||
|
||||
def write_event(self, event):
|
||||
"""Writes an ``Event`` object to Splunk.
|
||||
|
||||
:param event: An ``Event`` object.
|
||||
"""
|
||||
|
||||
if not self.header_written:
|
||||
self._out.write("<stream>")
|
||||
self.header_written = True
|
||||
|
||||
event.write_to(self._out)
|
||||
|
||||
def log(self, severity, message):
|
||||
"""Logs messages about the state of this modular input to Splunk.
|
||||
These messages will show up in Splunk's internal logs.
|
||||
|
||||
:param severity: ``string``, severity of message, see severities defined as class constants.
|
||||
:param message: ``string``, message to log.
|
||||
"""
|
||||
|
||||
self._err.write("%s %s\n" % (severity, message))
|
||||
self._err.flush()
|
||||
|
||||
def write_xml_document(self, document):
|
||||
"""Writes a string representation of an
|
||||
``ElementTree`` object to the output stream.
|
||||
|
||||
:param document: An ``ElementTree`` object.
|
||||
"""
|
||||
self._out.write(ET.tostring(document))
|
||||
self._out.flush()
|
||||
|
||||
def close(self):
|
||||
"""Write the closing </stream> tag to make this XML well formed."""
|
||||
self._out.write("</stream>")
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError as ie:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from .utils import parse_xml_data
|
||||
|
||||
class InputDefinition:
|
||||
"""``InputDefinition`` encodes the XML defining inputs that Splunk passes to
|
||||
a modular input script.
|
||||
|
||||
**Example**::
|
||||
|
||||
i = InputDefinition()
|
||||
|
||||
"""
|
||||
def __init__ (self):
|
||||
self.metadata = {}
|
||||
self.inputs = {}
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, InputDefinition):
|
||||
return False
|
||||
return self.metadata == other.metadata and self.inputs == other.inputs
|
||||
|
||||
@staticmethod
|
||||
def parse(stream):
|
||||
"""Parse a stream containing XML into an ``InputDefinition``.
|
||||
|
||||
:param stream: stream containing XML to parse.
|
||||
:return: definition: an ``InputDefinition`` object.
|
||||
"""
|
||||
definition = InputDefinition()
|
||||
|
||||
# parse XML from the stream, then get the root node
|
||||
root = ET.parse(stream).getroot()
|
||||
|
||||
for node in root:
|
||||
if node.tag == "configuration":
|
||||
# get config for each stanza
|
||||
definition.inputs = parse_xml_data(node, "stanza")
|
||||
else:
|
||||
definition.metadata[node.tag] = node.text
|
||||
|
||||
return definition
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class Scheme(object):
|
||||
"""Class representing the metadata for a modular input kind.
|
||||
|
||||
A ``Scheme`` specifies a title, description, several options of how Splunk should run modular inputs of this
|
||||
kind, and a set of arguments which define a particular modular input's properties.
|
||||
|
||||
The primary use of ``Scheme`` is to abstract away the construction of XML to feed to Splunk.
|
||||
"""
|
||||
|
||||
# Constant values, do not change
|
||||
# These should be used for setting the value of a Scheme object's streaming_mode field.
|
||||
streaming_mode_simple = "SIMPLE"
|
||||
streaming_mode_xml = "XML"
|
||||
|
||||
def __init__(self, title):
|
||||
"""
|
||||
:param title: ``string`` identifier for this Scheme in Splunk.
|
||||
"""
|
||||
self.title = title
|
||||
self.description = None
|
||||
self.use_external_validation = True
|
||||
self.use_single_instance = False
|
||||
self.streaming_mode = Scheme.streaming_mode_xml
|
||||
|
||||
# list of Argument objects, each to be represented by an <arg> tag
|
||||
self.arguments = []
|
||||
|
||||
def add_argument(self, arg):
|
||||
"""Add the provided argument, ``arg``, to the ``self.arguments`` list.
|
||||
|
||||
:param arg: An ``Argument`` object to add to ``self.arguments``.
|
||||
"""
|
||||
self.arguments.append(arg)
|
||||
|
||||
def to_xml(self):
|
||||
"""Creates an ``ET.Element`` representing self, then returns it.
|
||||
|
||||
:returns root, an ``ET.Element`` representing this scheme.
|
||||
"""
|
||||
root = ET.Element("scheme")
|
||||
|
||||
ET.SubElement(root, "title").text = self.title
|
||||
|
||||
# add a description subelement if it's defined
|
||||
if self.description is not None:
|
||||
ET.SubElement(root, "description").text = self.description
|
||||
|
||||
# add all other subelements to this Scheme, represented by (tag, text)
|
||||
subelements = [
|
||||
("use_external_validation", self.use_external_validation),
|
||||
("use_single_instance", self.use_single_instance),
|
||||
("streaming_mode", self.streaming_mode)
|
||||
]
|
||||
for name, value in subelements:
|
||||
ET.SubElement(root, name).text = str(value).lower()
|
||||
|
||||
endpoint = ET.SubElement(root, "endpoint")
|
||||
|
||||
args = ET.SubElement(endpoint, "args")
|
||||
|
||||
# add arguments as subelements to the <args> element
|
||||
for arg in self.arguments:
|
||||
arg.add_to_document(args)
|
||||
|
||||
return root
|
||||
@@ -0,0 +1,176 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from urlparse import urlsplit
|
||||
import sys
|
||||
|
||||
from ..client import Service
|
||||
from .event_writer import EventWriter
|
||||
from .input_definition import InputDefinition
|
||||
from .validation_definition import ValidationDefinition
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class Script(object):
|
||||
"""An abstract base class for implementing modular inputs.
|
||||
|
||||
Subclasses should override ``get_scheme``, ``stream_events``,
|
||||
and optionally ``validate_input`` if the modular input uses
|
||||
external validation.
|
||||
|
||||
The ``run`` function is used to run modular inputs; it typically should
|
||||
not be overridden.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __init__(self):
|
||||
self._input_definition = None
|
||||
self._service = None
|
||||
|
||||
def run(self, args):
|
||||
"""Runs this modular input
|
||||
|
||||
:param args: List of command line arguments passed to this script.
|
||||
:returns: An integer to be used as the exit value of this program.
|
||||
"""
|
||||
|
||||
# call the run_script function, which handles the specifics of running
|
||||
# a modular input
|
||||
return self.run_script(args, EventWriter(), sys.stdin)
|
||||
|
||||
def run_script(self, args, event_writer, input_stream):
|
||||
"""Handles all the specifics of running a modular input
|
||||
|
||||
:param args: List of command line arguments passed to this script.
|
||||
:param event_writer: An ``EventWriter`` object for writing events.
|
||||
:param input_stream: An input stream for reading inputs.
|
||||
:returns: An integer to be used as the exit value of this program.
|
||||
"""
|
||||
|
||||
try:
|
||||
if len(args) == 1:
|
||||
# This script is running as an input. Input definitions will be
|
||||
# passed on stdin as XML, and the script will write events on
|
||||
# stdout and log entries on stderr.
|
||||
self._input_definition = InputDefinition.parse(input_stream)
|
||||
self.stream_events(self._input_definition, event_writer)
|
||||
event_writer.close()
|
||||
return 0
|
||||
|
||||
elif str(args[1]).lower() == "--scheme":
|
||||
# Splunk has requested XML specifying the scheme for this
|
||||
# modular input Return it and exit.
|
||||
scheme = self.get_scheme()
|
||||
if scheme is None:
|
||||
event_writer.log(
|
||||
EventWriter.FATAL,
|
||||
"Modular input script returned a null scheme.")
|
||||
return 1
|
||||
else:
|
||||
event_writer.write_xml_document(scheme.to_xml())
|
||||
return 0
|
||||
|
||||
elif args[1].lower() == "--validate-arguments":
|
||||
validation_definition = ValidationDefinition.parse(input_stream)
|
||||
try:
|
||||
self.validate_input(validation_definition)
|
||||
return 0
|
||||
except Exception as e:
|
||||
root = ET.Element("error")
|
||||
ET.SubElement(root, "message").text = str(e)
|
||||
event_writer.write_xml_document(root)
|
||||
|
||||
return 1
|
||||
else:
|
||||
err_string = "ERROR Invalid arguments to modular input script:" + ' '.join(
|
||||
args)
|
||||
event_writer._err.write(err_string)
|
||||
|
||||
except Exception as e:
|
||||
err_string = EventWriter.ERROR + str(e.message)
|
||||
event_writer._err.write(err_string)
|
||||
return 1
|
||||
|
||||
@property
|
||||
def service(self):
|
||||
""" Returns a Splunk service object for this script invocation.
|
||||
|
||||
The service object is created from the Splunkd URI and session key
|
||||
passed to the command invocation on the modular input stream. It is
|
||||
available as soon as the :code:`Script.stream_events` method is
|
||||
called.
|
||||
|
||||
:return: :class:splunklib.client.Service. A value of None is returned,
|
||||
if you call this method before the :code:`Script.stream_events` method
|
||||
is called.
|
||||
|
||||
"""
|
||||
if self._service is not None:
|
||||
return self._service
|
||||
|
||||
if self._input_definition is None:
|
||||
return None
|
||||
|
||||
splunkd_uri = self._input_definition.metadata["server_uri"]
|
||||
session_key = self._input_definition.metadata["session_key"]
|
||||
|
||||
splunkd = urlsplit(splunkd_uri, allow_fragments=False)
|
||||
|
||||
self._service = Service(
|
||||
scheme=splunkd.scheme,
|
||||
host=splunkd.hostname,
|
||||
port=splunkd.port,
|
||||
token=session_key,
|
||||
)
|
||||
|
||||
return self._service
|
||||
|
||||
@abstractmethod
|
||||
def get_scheme(self):
|
||||
"""The scheme defines the parameters understood by this modular input.
|
||||
|
||||
:return: a ``Scheme`` object representing the parameters for this modular input.
|
||||
"""
|
||||
|
||||
def validate_input(self, definition):
|
||||
"""Handles external validation for modular input kinds.
|
||||
|
||||
When Splunk calls a modular input script in validation mode, it will
|
||||
pass in an XML document giving information about the Splunk instance (so
|
||||
you can call back into it if needed) and the name and parameters of the
|
||||
proposed input.
|
||||
|
||||
If this function does not throw an exception, the validation is assumed
|
||||
to succeed. Otherwise any errors thrown will be turned into a string and
|
||||
logged back to Splunk.
|
||||
|
||||
The default implementation always passes.
|
||||
|
||||
:param definition: The parameters for the proposed input passed by splunkd.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stream_events(self, inputs, ew):
|
||||
"""The method called to stream events into Splunk. It should do all of its output via
|
||||
EventWriter rather than assuming that there is a console attached.
|
||||
|
||||
:param inputs: An ``InputDefinition`` object.
|
||||
:param ew: An object with methods to write events and log messages to Splunk.
|
||||
"""
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# File for utility functions
|
||||
|
||||
def xml_compare(expected, found):
|
||||
"""Checks equality of two ``ElementTree`` objects.
|
||||
|
||||
:param expected: An ``ElementTree`` object.
|
||||
:param found: An ``ElementTree`` object.
|
||||
:return: ``Boolean``, whether the two objects are equal.
|
||||
"""
|
||||
|
||||
# if comparing the same ET object
|
||||
if expected == found:
|
||||
return True
|
||||
|
||||
# compare element attributes, ignoring order
|
||||
if set(expected.items()) != set(found.items()):
|
||||
return False
|
||||
|
||||
# check for equal number of children
|
||||
expected_children = list(expected)
|
||||
found_children = list(found)
|
||||
if len(expected_children) != len(found_children):
|
||||
return False
|
||||
|
||||
# compare children
|
||||
if not all([xml_compare(a, b) for a, b in zip(expected_children, found_children)]):
|
||||
return False
|
||||
|
||||
# compare elements, if there is no text node, return True
|
||||
if (expected.text is None or expected.text.strip() == "") \
|
||||
and (found.text is None or found.text.strip() == ""):
|
||||
return True
|
||||
else:
|
||||
return expected.tag == found.tag and expected.text == found.text \
|
||||
and expected.attrib == found.attrib
|
||||
|
||||
def parse_parameters(param_node):
|
||||
if param_node.tag == "param":
|
||||
return param_node.text
|
||||
elif param_node.tag == "param_list":
|
||||
parameters = []
|
||||
for mvp in param_node:
|
||||
parameters.append(mvp.text)
|
||||
return parameters
|
||||
else:
|
||||
raise ValueError("Invalid configuration scheme, %s tag unexpected." % param_node.tag)
|
||||
|
||||
def parse_xml_data(parent_node, child_node_tag):
|
||||
data = {}
|
||||
for child in parent_node:
|
||||
if child.tag == child_node_tag:
|
||||
if child_node_tag == "stanza":
|
||||
data[child.get("name")] = {}
|
||||
for param in child:
|
||||
data[child.get("name")][param.get("name")] = parse_parameters(param)
|
||||
elif "item" == parent_node.tag:
|
||||
data[child.get("name")] = parse_parameters(child)
|
||||
return data
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError as ie:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from .utils import parse_xml_data
|
||||
|
||||
|
||||
class ValidationDefinition(object):
|
||||
"""This class represents the XML sent by Splunk for external validation of a
|
||||
new modular input.
|
||||
|
||||
**Example**::
|
||||
|
||||
``v = ValidationDefinition()``
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self.metadata = {}
|
||||
self.parameters = {}
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, ValidationDefinition):
|
||||
return False
|
||||
return self.metadata == other.metadata and self.parameters == other.parameters
|
||||
|
||||
@staticmethod
|
||||
def parse(stream):
|
||||
"""Creates a ``ValidationDefinition`` from a provided stream containing XML.
|
||||
|
||||
The XML typically will look like this:
|
||||
|
||||
``<items>``
|
||||
`` <server_host>myHost</server_host>``
|
||||
`` <server_uri>https://127.0.0.1:8089</server_uri>``
|
||||
`` <session_key>123102983109283019283</session_key>``
|
||||
`` <checkpoint_dir>/opt/splunk/var/lib/splunk/modinputs</checkpoint_dir>``
|
||||
`` <item name="myScheme">``
|
||||
`` <param name="param1">value1</param>``
|
||||
`` <param_list name="param2">``
|
||||
`` <value>value2</value>``
|
||||
`` <value>value3</value>``
|
||||
`` <value>value4</value>``
|
||||
`` </param_list>``
|
||||
`` </item>``
|
||||
``</items>``
|
||||
|
||||
:param stream: ``Stream`` containing XML to parse.
|
||||
:return definition: A ``ValidationDefinition`` object.
|
||||
|
||||
"""
|
||||
|
||||
definition = ValidationDefinition()
|
||||
|
||||
# parse XML from the stream, then get the root node
|
||||
root = ET.parse(stream).getroot()
|
||||
|
||||
for node in root:
|
||||
# lone item node
|
||||
if node.tag == "item":
|
||||
# name from item node
|
||||
definition.metadata["name"] = node.get("name")
|
||||
definition.parameters = parse_xml_data(node, "")
|
||||
else:
|
||||
# Store anything else in metadata
|
||||
definition.metadata[node.tag] = node.text
|
||||
|
||||
return definition
|
||||
@@ -0,0 +1,128 @@
|
||||
# Copyright (c) 2009 Raymond Hettinger
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from UserDict import DictMixin
|
||||
|
||||
|
||||
class OrderedDict(dict, DictMixin):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__end
|
||||
except AttributeError:
|
||||
self.clear()
|
||||
self.update(*args, **kwds)
|
||||
|
||||
def clear(self):
|
||||
self.__end = end = []
|
||||
end += [None, end, end] # sentinel node for doubly linked list
|
||||
self.__map = {} # key --> [key, prev, next]
|
||||
dict.clear(self)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
key, prev, next = self.__map.pop(key)
|
||||
prev[2] = next
|
||||
next[1] = prev
|
||||
|
||||
def __iter__(self):
|
||||
end = self.__end
|
||||
curr = end[2]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[2]
|
||||
|
||||
def __reversed__(self):
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[1]
|
||||
|
||||
def popitem(self, last=True):
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
if last:
|
||||
key = reversed(self).next()
|
||||
else:
|
||||
key = iter(self).next()
|
||||
value = self.pop(key)
|
||||
return key, value
|
||||
|
||||
def __reduce__(self):
|
||||
items = [[k, self[k]] for k in self]
|
||||
tmp = self.__map, self.__end
|
||||
del self.__map, self.__end
|
||||
inst_dict = vars(self).copy()
|
||||
self.__map, self.__end = tmp
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
setdefault = DictMixin.setdefault
|
||||
update = DictMixin.update
|
||||
pop = DictMixin.pop
|
||||
values = DictMixin.values
|
||||
items = DictMixin.items
|
||||
iterkeys = DictMixin.iterkeys
|
||||
itervalues = DictMixin.itervalues
|
||||
iteritems = DictMixin.iteritems
|
||||
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, OrderedDict):
|
||||
if len(self) != len(other):
|
||||
return False
|
||||
for p, q in zip(self.items(), other.items()):
|
||||
if p != q:
|
||||
return False
|
||||
return True
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
@@ -0,0 +1,288 @@
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""The **splunklib.results** module provides a streaming XML reader for Splunk
|
||||
search results.
|
||||
|
||||
Splunk search results can be returned in a variety of formats including XML,
|
||||
JSON, and CSV. To make it easier to stream search results in XML format, they
|
||||
are returned as a stream of XML *fragments*, not as a single XML document. This
|
||||
module supports incrementally reading one result record at a time from such a
|
||||
result stream. This module also provides a friendly iterator-based interface for
|
||||
accessing search results while avoiding buffering the result set, which can be
|
||||
very large.
|
||||
|
||||
To use the reader, instantiate :class:`ResultsReader` on a search result stream
|
||||
as follows:::
|
||||
|
||||
reader = ResultsReader(result_stream)
|
||||
for item in reader:
|
||||
print(item)
|
||||
print "Results are a preview: %s" % reader.is_preview
|
||||
"""
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as et
|
||||
except:
|
||||
import xml.etree.ElementTree as et
|
||||
|
||||
try:
|
||||
from collections import OrderedDict # must be python 2.7
|
||||
except ImportError:
|
||||
from .ordereddict import OrderedDict
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except:
|
||||
from StringIO import StringIO
|
||||
|
||||
__all__ = [
|
||||
"ResultsReader",
|
||||
"Message"
|
||||
]
|
||||
|
||||
class Message(object):
|
||||
"""This class represents informational messages that Splunk interleaves in the results stream.
|
||||
|
||||
``Message`` takes two arguments: a string giving the message type (e.g., "DEBUG"), and
|
||||
a string giving the message itself.
|
||||
|
||||
**Example**::
|
||||
|
||||
m = Message("DEBUG", "There's something in that variable...")
|
||||
"""
|
||||
def __init__(self, type_, message):
|
||||
self.type = type_
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return "%s: %s" % (self.type, self.message)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.type, self.message) == (other.type, other.message)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.type, self.message))
|
||||
|
||||
class _ConcatenatedStream(object):
|
||||
"""Lazily concatenate zero or more streams into a stream.
|
||||
|
||||
As you read from the concatenated stream, you get characters from
|
||||
each stream passed to ``_ConcatenatedStream``, in order.
|
||||
|
||||
**Example**::
|
||||
|
||||
from StringIO import StringIO
|
||||
s = _ConcatenatedStream(StringIO("abc"), StringIO("def"))
|
||||
assert s.read() == "abcdef"
|
||||
"""
|
||||
def __init__(self, *streams):
|
||||
self.streams = list(streams)
|
||||
|
||||
def read(self, n=None):
|
||||
"""Read at most *n* characters from this stream.
|
||||
|
||||
If *n* is ``None``, return all available characters.
|
||||
"""
|
||||
response = ""
|
||||
while len(self.streams) > 0 and (n is None or n > 0):
|
||||
txt = self.streams[0].read(n)
|
||||
response += txt
|
||||
if n is not None:
|
||||
n -= len(txt)
|
||||
if n > 0 or n is None:
|
||||
del self.streams[0]
|
||||
return response
|
||||
|
||||
class _XMLDTDFilter(object):
|
||||
"""Lazily remove all XML DTDs from a stream.
|
||||
|
||||
All substrings matching the regular expression <?[^>]*> are
|
||||
removed in their entirety from the stream. No regular expressions
|
||||
are used, however, so everything still streams properly.
|
||||
|
||||
**Example**::
|
||||
|
||||
from StringIO import StringIO
|
||||
s = _XMLDTDFilter("<?xml abcd><element><?xml ...></element>")
|
||||
assert s.read() == "<element></element>"
|
||||
"""
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
def read(self, n=None):
|
||||
"""Read at most *n* characters from this stream.
|
||||
|
||||
If *n* is ``None``, return all available characters.
|
||||
"""
|
||||
response = ""
|
||||
while n is None or n > 0:
|
||||
c = self.stream.read(1)
|
||||
if c == "":
|
||||
break
|
||||
elif c == "<":
|
||||
c += self.stream.read(1)
|
||||
if c == "<?":
|
||||
while True:
|
||||
q = self.stream.read(1)
|
||||
if q == ">":
|
||||
break
|
||||
else:
|
||||
response += c
|
||||
if n is not None:
|
||||
n -= len(c)
|
||||
else:
|
||||
response += c
|
||||
if n is not None:
|
||||
n -= 1
|
||||
return response
|
||||
|
||||
class ResultsReader(object):
|
||||
"""This class returns dictionaries and Splunk messages from an XML results
|
||||
stream.
|
||||
|
||||
``ResultsReader`` is iterable, and returns a ``dict`` for results, or a
|
||||
:class:`Message` object for Splunk messages. This class has one field,
|
||||
``is_preview``, which is ``True`` when the results are a preview from a
|
||||
running search, or ``False`` when the results are from a completed search.
|
||||
|
||||
This function has no network activity other than what is implicit in the
|
||||
stream it operates on.
|
||||
|
||||
:param `stream`: The stream to read from (any object that supports
|
||||
``.read()``).
|
||||
|
||||
**Example**::
|
||||
|
||||
import results
|
||||
response = ... # the body of an HTTP response
|
||||
reader = results.ResultsReader(response)
|
||||
for result in reader:
|
||||
if isinstance(result, dict):
|
||||
print "Result: %s" % result
|
||||
elif isinstance(result, results.Message):
|
||||
print "Message: %s" % result
|
||||
print "is_preview = %s " % reader.is_preview
|
||||
"""
|
||||
# Be sure to update the docstrings of client.Jobs.oneshot,
|
||||
# client.Job.results_preview and client.Job.results to match any
|
||||
# changes made to ResultsReader.
|
||||
#
|
||||
# This wouldn't be a class, just the _parse_results function below,
|
||||
# except that you cannot get the current generator inside the
|
||||
# function creating that generator. Thus it's all wrapped up for
|
||||
# the sake of one field.
|
||||
def __init__(self, stream):
|
||||
# The search/jobs/exports endpoint, when run with
|
||||
# earliest_time=rt and latest_time=rt streams a sequence of
|
||||
# XML documents, each containing a result, as opposed to one
|
||||
# results element containing lots of results. Python's XML
|
||||
# parsers are broken, and instead of reading one full document
|
||||
# and returning the stream that follows untouched, they
|
||||
# destroy the stream and throw an error. To get around this,
|
||||
# we remove all the DTD definitions inline, then wrap the
|
||||
# fragments in a fiction <doc> element to make the parser happy.
|
||||
stream = _XMLDTDFilter(stream)
|
||||
stream = _ConcatenatedStream(StringIO("<doc>"), stream, StringIO("</doc>"))
|
||||
self.is_preview = None
|
||||
self._gen = self._parse_results(stream)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
return self._gen.next()
|
||||
|
||||
def _parse_results(self, stream):
|
||||
"""Parse results and messages out of *stream*."""
|
||||
result = None
|
||||
values = None
|
||||
try:
|
||||
for event, elem in et.iterparse(stream, events=('start', 'end')):
|
||||
if elem.tag == 'results' and event == 'start':
|
||||
# The wrapper element is a <results preview="0|1">. We
|
||||
# don't care about it except to tell is whether these
|
||||
# are preview results, or the final results from the
|
||||
# search.
|
||||
is_preview = elem.attrib['preview'] == '1'
|
||||
self.is_preview = is_preview
|
||||
if elem.tag == 'result':
|
||||
if event == 'start':
|
||||
result = OrderedDict()
|
||||
elif event == 'end':
|
||||
yield result
|
||||
result = None
|
||||
elem.clear()
|
||||
|
||||
elif elem.tag == 'field' and result is not None:
|
||||
# We need the 'result is not None' check because
|
||||
# 'field' is also the element name in the <meta>
|
||||
# header that gives field order, which is not what we
|
||||
# want at all.
|
||||
if event == 'start':
|
||||
values = []
|
||||
elif event == 'end':
|
||||
field_name = elem.attrib['k'].encode('utf8')
|
||||
if len(values) == 1:
|
||||
result[field_name] = values[0]
|
||||
else:
|
||||
result[field_name] = values
|
||||
# Calling .clear() is necessary to let the
|
||||
# element be garbage collected. Otherwise
|
||||
# arbitrarily large results sets will use
|
||||
# arbitrarily large memory intead of
|
||||
# streaming.
|
||||
elem.clear()
|
||||
|
||||
elif elem.tag in ('text', 'v') and event == 'end':
|
||||
try:
|
||||
text = "".join(elem.itertext())
|
||||
except AttributeError:
|
||||
# Assume we're running in Python < 2.7, before itertext() was added
|
||||
# So we'll define it here
|
||||
|
||||
def __itertext(self):
|
||||
tag = self.tag
|
||||
if not isinstance(tag, basestring) and tag is not None:
|
||||
return
|
||||
if self.text:
|
||||
yield self.text
|
||||
for e in self:
|
||||
for s in __itertext(e):
|
||||
yield s
|
||||
if e.tail:
|
||||
yield e.tail
|
||||
|
||||
text = "".join(__itertext(elem))
|
||||
values.append(text.encode('utf8'))
|
||||
elem.clear()
|
||||
|
||||
elif elem.tag == 'msg':
|
||||
if event == 'start':
|
||||
msg_type = elem.attrib['type']
|
||||
elif event == 'end':
|
||||
text = elem.text if elem.text is not None else ""
|
||||
yield Message(msg_type, text.encode('utf8'))
|
||||
elem.clear()
|
||||
except SyntaxError as pe:
|
||||
# This is here to handle the same incorrect return from
|
||||
# splunk that is described in __init__.
|
||||
if 'no element found' in pe.msg:
|
||||
return
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright © 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
|
||||
.. topic:: Design Notes
|
||||
|
||||
1. Commands are constrained to this ABNF grammar::
|
||||
|
||||
command = command-name *[wsp option] *[wsp [dquote] field-name [dquote]]
|
||||
command-name = alpha *( alpha / digit )
|
||||
option = option-name [wsp] "=" [wsp] option-value
|
||||
option-name = alpha *( alpha / digit / "_" )
|
||||
option-value = word / quoted-string
|
||||
word = 1*( %01-%08 / %0B / %0C / %0E-1F / %21 / %23-%FF ) ; Any character but DQUOTE and WSP
|
||||
quoted-string = dquote *( word / wsp / "\" dquote / dquote dquote ) dquote
|
||||
field-name = ( "_" / alpha ) *( alpha / digit / "_" / "." / "-" )
|
||||
|
||||
It does not show that :code:`field-name` values may be comma-separated. This is because Splunk strips commas from
|
||||
the command line. A search command will never see them.
|
||||
|
||||
2. Search commands targeting versions of Splunk prior to 6.3 must be statically configured as follows:
|
||||
|
||||
.. code-block:: text
|
||||
:linenos:
|
||||
|
||||
[command_name]
|
||||
filename = command_name.py
|
||||
supports_getinfo = true
|
||||
supports_rawargs = true
|
||||
|
||||
No other static configuration is required or expected and may interfere with command execution.
|
||||
|
||||
3. Commands support dynamic probing for settings.
|
||||
|
||||
Splunk probes for settings dynamically when :code:`supports_getinfo=true`.
|
||||
You must add this line to the commands.conf stanza for each of your search
|
||||
commands.
|
||||
|
||||
4. Commands do not support parsed arguments on the command line.
|
||||
|
||||
Splunk parses arguments when :code:`supports_rawargs=false`. The
|
||||
:code:`SearchCommand` class sets this value unconditionally. You cannot
|
||||
override it.
|
||||
|
||||
**Rationale**
|
||||
|
||||
Splunk parses arguments by stripping quotes, nothing more. This may be useful
|
||||
in some cases, but doesn't work well with our chosen grammar.
|
||||
|
||||
5. Commands consume input headers.
|
||||
|
||||
An input header is provided by Splunk when :code:`enableheader=true`. The
|
||||
:class:`SearchCommand` class sets this value unconditionally. You cannot
|
||||
override it.
|
||||
|
||||
6. Commands produce an output messages header.
|
||||
|
||||
Splunk expects a command to produce an output messages header when
|
||||
:code:`outputheader=true`. The :class:`SearchCommand` class sets this value
|
||||
unconditionally. You cannot override it.
|
||||
|
||||
7. Commands support multi-value fields.
|
||||
|
||||
Multi-value fields are provided and consumed by Splunk when
|
||||
:code:`supports_multivalue=true`. This value is fixed. You cannot override
|
||||
it.
|
||||
|
||||
8. This module represents all fields on the output stream in multi-value
|
||||
format.
|
||||
|
||||
Splunk recognizes two kinds of data: :code:`value` and :code:`list(value)`.
|
||||
The multi-value format represents these data in field pairs. Given field
|
||||
:code:`name` the multi-value format calls for the creation of this pair of
|
||||
fields.
|
||||
|
||||
================= =========================================================
|
||||
Field name Field data
|
||||
================= =========================================================
|
||||
:code:`name` Value or text from which a list of values was derived.
|
||||
|
||||
:code:`__mv_name` Empty, if :code:`field` represents a :code:`value`;
|
||||
otherwise, an encoded :code:`list(value)`. Values in the
|
||||
list are wrapped in dollar signs ($) and separated by
|
||||
semi-colons (;). Dollar signs ($) within a value are
|
||||
represented by a pair of dollar signs ($$).
|
||||
================= =========================================================
|
||||
|
||||
Serializing data in this format enables streaming and reduces a command's
|
||||
memory footprint at the cost of one extra byte of data per field per record
|
||||
and a small amount of extra processing time by the next command in the
|
||||
pipeline.
|
||||
|
||||
9. A :class:`ReportingCommand` must override :meth:`~ReportingCommand.reduce`
|
||||
and may override :meth:`~ReportingCommand.map`. Map/reduce commands on the
|
||||
Splunk processing pipeline are distinguished as this example illustrates.
|
||||
|
||||
**Splunk command**
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
sum total=total_date_hour date_hour
|
||||
|
||||
**Map command line**
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
sum __GETINFO__ __map__ total=total_date_hour date_hour
|
||||
sum __EXECUTE__ __map__ total=total_date_hour date_hour
|
||||
|
||||
**Reduce command line**
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
sum __GETINFO__ total=total_date_hour date_hour
|
||||
sum __EXECUTE__ total=total_date_hour date_hour
|
||||
|
||||
The :code:`__map__` argument is introduced by
|
||||
:meth:`ReportingCommand._execute`. Search command authors cannot influence
|
||||
the contents of the command line in this release.
|
||||
|
||||
.. topic:: References
|
||||
|
||||
1. `Search command style guide <http://docs.splunk.com/Documentation/Splunk/6.0/Search/Searchcommandstyleguide>`_
|
||||
|
||||
2. `Commands.conf.spec <http://docs.splunk.com/Documentation/Splunk/5.0.5/Admin/Commandsconf>`_
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from .environment import *
|
||||
from .decorators import *
|
||||
from .validators import *
|
||||
|
||||
from .generating_command import GeneratingCommand
|
||||
from .streaming_command import StreamingCommand
|
||||
from .eventing_command import EventingCommand
|
||||
from .reporting_command import ReportingCommand
|
||||
|
||||
from .external_search_command import execute, ExternalSearchCommand
|
||||
from .search_command import dispatch, SearchMetric
|
||||
@@ -0,0 +1,447 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright © 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
try:
|
||||
from collections import OrderedDict # must be python 2.7
|
||||
except ImportError:
|
||||
from ..ordereddict import OrderedDict
|
||||
|
||||
from inspect import getmembers, isclass, isfunction
|
||||
from itertools import imap
|
||||
|
||||
from .internals import ConfigurationSettingsType, json_encode_string
|
||||
from .validators import OptionName
|
||||
|
||||
|
||||
class Configuration(object):
|
||||
""" Defines the configuration settings for a search command.
|
||||
|
||||
Documents, validates, and ensures that only relevant configuration settings are applied. Adds a :code:`name` class
|
||||
variable to search command classes that don't have one. The :code:`name` is derived from the name of the class.
|
||||
By convention command class names end with the word "Command". To derive :code:`name` the word "Command" is removed
|
||||
from the end of the class name and then converted to lower case for conformance with the `Search command style guide
|
||||
<http://docs.splunk.com/Documentation/Splunk/latest/Search/Searchcommandstyleguide>`_
|
||||
|
||||
"""
|
||||
def __init__(self, o=None, **kwargs):
|
||||
#
|
||||
# The o argument enables the configuration decorator to be used with or without parentheses. For example, it
|
||||
# enables you to write code that looks like this:
|
||||
#
|
||||
# @Configuration
|
||||
# class Foo(SearchCommand):
|
||||
# ...
|
||||
#
|
||||
# @Configuration()
|
||||
# class Bar(SearchCommand):
|
||||
# ...
|
||||
#
|
||||
# Without the o argument, the Python compiler will complain about the first form. With the o argument, both
|
||||
# forms work. The first form provides a value for o: Foo. The second form does does not provide a value for o.
|
||||
# The class or method decorated is not passed to the constructor. A value of None is passed instead.
|
||||
#
|
||||
self.settings = kwargs
|
||||
|
||||
def __call__(self, o):
|
||||
|
||||
if isfunction(o):
|
||||
# We must wait to finalize configuration as the class containing this function is under construction
|
||||
# at the time this call to decorate a member function. This will be handled in the call to
|
||||
# o.ConfigurationSettings.fix_up(o) in the elif clause of this code block.
|
||||
o._settings = self.settings
|
||||
elif isclass(o):
|
||||
|
||||
# Set command name
|
||||
|
||||
name = o.__name__
|
||||
if name.endswith(b'Command'):
|
||||
name = name[:-len(b'Command')]
|
||||
o.name = unicode(name.lower())
|
||||
|
||||
# Construct ConfigurationSettings instance for the command class
|
||||
|
||||
o.ConfigurationSettings = ConfigurationSettingsType(
|
||||
module=o.__module__ + b'.' + o.__name__,
|
||||
name=b'ConfigurationSettings',
|
||||
bases=(o.ConfigurationSettings,))
|
||||
|
||||
ConfigurationSetting.fix_up(o.ConfigurationSettings, self.settings)
|
||||
o.ConfigurationSettings.fix_up(o)
|
||||
Option.fix_up(o)
|
||||
else:
|
||||
raise TypeError('Incorrect usage: Configuration decorator applied to {0}'.format(type(o), o.__name__))
|
||||
|
||||
return o
|
||||
|
||||
|
||||
class ConfigurationSetting(property):
|
||||
""" Generates a :class:`property` representing the named configuration setting
|
||||
|
||||
This is a convenience function designed to reduce the amount of boiler-plate code you must write; most notably for
|
||||
property setters.
|
||||
|
||||
:param name: Configuration setting name.
|
||||
:type name: str or unicode
|
||||
|
||||
:param doc: A documentation string.
|
||||
:type doc: bytes, unicode or NoneType
|
||||
|
||||
:param readonly: If true, specifies that the configuration setting is fixed.
|
||||
:type name: bool or NoneType
|
||||
|
||||
:param value: Configuration setting value.
|
||||
|
||||
:return: A :class:`property` instance representing the configuration setting.
|
||||
:rtype: property
|
||||
|
||||
"""
|
||||
def __init__(self, fget=None, fset=None, fdel=None, doc=None, name=None, readonly=None, value=None):
|
||||
property.__init__(self, fget=fget, fset=fset, fdel=fdel, doc=doc)
|
||||
self._readonly = readonly
|
||||
self._value = value
|
||||
self._name = name
|
||||
|
||||
def __call__(self, function):
|
||||
return self.getter(function)
|
||||
|
||||
def deleter(self, function):
|
||||
return self._copy_extra_attributes(property.deleter(self, function))
|
||||
|
||||
def getter(self, function):
|
||||
return self._copy_extra_attributes(property.getter(self, function))
|
||||
|
||||
def setter(self, function):
|
||||
return self._copy_extra_attributes(property.setter(self, function))
|
||||
|
||||
@staticmethod
|
||||
def fix_up(cls, values):
|
||||
|
||||
is_configuration_setting = lambda attribute: isinstance(attribute, ConfigurationSetting)
|
||||
definitions = getmembers(cls, is_configuration_setting)
|
||||
i = 0
|
||||
|
||||
for name, setting in definitions:
|
||||
|
||||
if setting._name is None:
|
||||
setting._name = name = unicode(name)
|
||||
else:
|
||||
name = setting._name
|
||||
|
||||
validate, specification = setting._get_specification()
|
||||
backing_field_name = '_' + name
|
||||
|
||||
if setting.fget is None and setting.fset is None and setting.fdel is None:
|
||||
|
||||
value = setting._value
|
||||
|
||||
if setting._readonly or value is not None:
|
||||
validate(specification, name, value)
|
||||
|
||||
def fget(bfn, value):
|
||||
return lambda this: getattr(this, bfn, value)
|
||||
|
||||
setting = setting.getter(fget(backing_field_name, value))
|
||||
|
||||
if not setting._readonly:
|
||||
|
||||
def fset(bfn, validate, specification, name):
|
||||
return lambda this, value: setattr(this, bfn, validate(specification, name, value))
|
||||
|
||||
setting = setting.setter(fset(backing_field_name, validate, specification, name))
|
||||
|
||||
setattr(cls, name, setting)
|
||||
|
||||
def is_supported_by_protocol(supporting_protocols):
|
||||
|
||||
def is_supported_by_protocol(version):
|
||||
return version in supporting_protocols
|
||||
|
||||
return is_supported_by_protocol
|
||||
|
||||
del setting._name, setting._value, setting._readonly
|
||||
|
||||
setting.is_supported_by_protocol = is_supported_by_protocol(specification.supporting_protocols)
|
||||
setting.supporting_protocols = specification.supporting_protocols
|
||||
setting.backing_field_name = backing_field_name
|
||||
definitions[i] = setting
|
||||
setting.name = name
|
||||
|
||||
i += 1
|
||||
|
||||
try:
|
||||
value = values[name]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if setting.fset is None:
|
||||
raise ValueError('The value of configuration setting {} is fixed'.format(name))
|
||||
|
||||
setattr(cls, backing_field_name, validate(specification, name, value))
|
||||
del values[name]
|
||||
|
||||
if len(values) > 0:
|
||||
settings = sorted(list(values.iteritems()))
|
||||
settings = imap(lambda (n, v): '{}={}'.format(n, repr(v)), settings)
|
||||
raise AttributeError('Inapplicable configuration settings: ' + ', '.join(settings))
|
||||
|
||||
cls.configuration_setting_definitions = definitions
|
||||
|
||||
def _copy_extra_attributes(self, other):
|
||||
other._readonly = self._readonly
|
||||
other._value = self._value
|
||||
other._name = self._name
|
||||
return other
|
||||
|
||||
def _get_specification(self):
|
||||
|
||||
name = self._name
|
||||
|
||||
try:
|
||||
specification = ConfigurationSettingsType.specification_matrix[name]
|
||||
except KeyError:
|
||||
raise AttributeError('Unknown configuration setting: {}={}'.format(name, repr(self._value)))
|
||||
|
||||
return ConfigurationSettingsType.validate_configuration_setting, specification
|
||||
|
||||
|
||||
class Option(property):
|
||||
""" Represents a search command option.
|
||||
|
||||
Required options must be specified on the search command line.
|
||||
|
||||
**Example:**
|
||||
|
||||
Short form (recommended). When you are satisfied with built-in or custom validation behaviors.
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
from splunklib.searchcommands.decorators import Option
|
||||
from splunklib.searchcommands.validators import Fieldname
|
||||
|
||||
total = Option(
|
||||
doc=''' **Syntax:** **total=***<fieldname>*
|
||||
**Description:** Name of the field that will hold the computed
|
||||
sum''',
|
||||
require=True, validate=Fieldname())
|
||||
|
||||
**Example:**
|
||||
|
||||
Long form. Useful when you wish to manage the option value and its deleter/getter/setter side-effects yourself. You
|
||||
must provide a getter and a setter. If your :code:`Option` requires `destruction <https://docs.python.org/2/reference/datamodel.html#object.__del__>`_ you must
|
||||
also provide a deleter. You must be prepared to accept a value of :const:`None` which indicates that your
|
||||
:code:`Option` is unset.
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
from splunklib.searchcommands import Option
|
||||
|
||||
@Option()
|
||||
def logging_configuration(self):
|
||||
\""" **Syntax:** logging_configuration=<path>
|
||||
**Description:** Loads an alternative logging configuration file for a command invocation. The logging
|
||||
configuration file must be in Python ConfigParser-format. The *<path>* name and all path names specified in
|
||||
configuration are relative to the app root directory.
|
||||
|
||||
\"""
|
||||
return self._logging_configuration
|
||||
|
||||
@logging_configuration.setter
|
||||
def logging_configuration(self, value):
|
||||
if value is not None
|
||||
logging.configure(value)
|
||||
self._logging_configuration = value
|
||||
|
||||
def __init__(self)
|
||||
self._logging_configuration = None
|
||||
|
||||
"""
|
||||
def __init__(self, fget=None, fset=None, fdel=None, doc=None, name=None, default=None, require=None, validate=None):
|
||||
property.__init__(self, fget, fset, fdel, doc)
|
||||
self.name = name
|
||||
self.default = default
|
||||
self.validate = validate
|
||||
self.require = bool(require)
|
||||
|
||||
def __call__(self, function):
|
||||
return self.getter(function)
|
||||
|
||||
# region Methods
|
||||
|
||||
def deleter(self, function):
|
||||
return self._copy_extra_attributes(property.deleter(self, function))
|
||||
|
||||
def getter(self, function):
|
||||
return self._copy_extra_attributes(property.getter(self, function))
|
||||
|
||||
def setter(self, function):
|
||||
return self._copy_extra_attributes(property.setter(self, function))
|
||||
|
||||
@classmethod
|
||||
def fix_up(cls, command_class):
|
||||
|
||||
is_option = lambda attribute: isinstance(attribute, Option)
|
||||
definitions = getmembers(command_class, is_option)
|
||||
validate_option_name = OptionName()
|
||||
i = 0
|
||||
|
||||
for name, option in definitions:
|
||||
|
||||
if option.name is None:
|
||||
option.name = name # no validation required
|
||||
else:
|
||||
validate_option_name(option.name)
|
||||
|
||||
if option.fget is None and option.fset is None and option.fdel is None:
|
||||
backing_field_name = '_' + name
|
||||
|
||||
def fget(bfn):
|
||||
return lambda this: getattr(this, bfn, None)
|
||||
|
||||
option = option.getter(fget(backing_field_name))
|
||||
|
||||
def fset(bfn, validate):
|
||||
if validate is None:
|
||||
return lambda this, value: setattr(this, bfn, value)
|
||||
return lambda this, value: setattr(this, bfn, validate(value))
|
||||
|
||||
option = option.setter(fset(backing_field_name, option.validate))
|
||||
setattr(command_class, name, option)
|
||||
|
||||
elif option.validate is not None:
|
||||
|
||||
def fset(function, validate):
|
||||
return lambda this, value: function(this, validate(value))
|
||||
|
||||
option = option.setter(fset(option.fset, option.validate))
|
||||
setattr(command_class, name, option)
|
||||
|
||||
definitions[i] = name, option
|
||||
i += 1
|
||||
|
||||
command_class.option_definitions = definitions
|
||||
|
||||
def _copy_extra_attributes(self, other):
|
||||
other.name = self.name
|
||||
other.default = self.default
|
||||
other.require = self.require
|
||||
other.validate = self.validate
|
||||
return other
|
||||
|
||||
# endregion
|
||||
|
||||
# region Types
|
||||
|
||||
class Item(object):
|
||||
""" Presents an instance/class view over a search command `Option`.
|
||||
|
||||
This class is used by SearchCommand.process to parse and report on option values.
|
||||
|
||||
"""
|
||||
def __init__(self, command, option):
|
||||
self._command = command
|
||||
self._option = option
|
||||
self._is_set = False
|
||||
validator = self.validator
|
||||
self._format = unicode if validator is None else validator.format
|
||||
|
||||
def __repr__(self):
|
||||
return '(' + repr(self.name) + ', ' + repr(self._format(self.value)) + ')'
|
||||
|
||||
def __str__(self):
|
||||
value = self.value
|
||||
value = 'None' if value is None else json_encode_string(self._format(value))
|
||||
return self.name + '=' + value
|
||||
|
||||
# region Properties
|
||||
|
||||
@property
|
||||
def is_required(self):
|
||||
return bool(self._option.require)
|
||||
|
||||
@property
|
||||
def is_set(self):
|
||||
""" Indicates whether an option value was provided as argument.
|
||||
|
||||
"""
|
||||
return self._is_set
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._option.name
|
||||
|
||||
@property
|
||||
def validator(self):
|
||||
return self._option.validate
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._option.__get__(self._command)
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self._option.__set__(self._command, value)
|
||||
self._is_set = True
|
||||
|
||||
# endregion
|
||||
|
||||
# region Methods
|
||||
|
||||
def reset(self):
|
||||
self._option.__set__(self._command, self._option.default)
|
||||
self._is_set = False
|
||||
|
||||
pass
|
||||
# endregion
|
||||
|
||||
class View(OrderedDict):
|
||||
""" Presents an ordered dictionary view of the set of :class:`Option` arguments to a search command.
|
||||
|
||||
This class is used by SearchCommand.process to parse and report on option values.
|
||||
|
||||
"""
|
||||
def __init__(self, command):
|
||||
definitions = type(command).option_definitions
|
||||
item_class = Option.Item
|
||||
OrderedDict.__init__(self, imap(lambda (name, option): (option.name, item_class(command, option)), definitions))
|
||||
|
||||
def __repr__(self):
|
||||
text = 'Option.View([' + ','.join(imap(lambda item: repr(item), self.itervalues())) + '])'
|
||||
return text
|
||||
|
||||
def __str__(self):
|
||||
text = ' '.join([str(item) for item in self.itervalues() if item.is_set])
|
||||
return text
|
||||
|
||||
# region Methods
|
||||
|
||||
def get_missing(self):
|
||||
missing = [item.name for item in self.itervalues() if item.is_required and not item.is_set]
|
||||
return missing if len(missing) > 0 else None
|
||||
|
||||
def reset(self):
|
||||
for value in self.itervalues():
|
||||
value.reset()
|
||||
|
||||
pass
|
||||
# endregion
|
||||
|
||||
pass
|
||||
# endregion
|
||||
|
||||
|
||||
__all__ = ['Configuration', 'Option']
|
||||
@@ -0,0 +1,122 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright © 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger, root, StreamHandler
|
||||
from logging.config import fileConfig
|
||||
from os import chdir, environ, getcwdu, path
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def configure_logging(logger_name, filename=None):
|
||||
""" Configure logging and return the named logger and the location of the logging configuration file loaded.
|
||||
|
||||
This function expects a Splunk app directory structure::
|
||||
|
||||
<app-root>
|
||||
bin
|
||||
...
|
||||
default
|
||||
...
|
||||
local
|
||||
...
|
||||
|
||||
This function looks for a logging configuration file at each of these locations, loading the first, if any,
|
||||
logging configuration file that it finds::
|
||||
|
||||
local/{name}.logging.conf
|
||||
default/{name}.logging.conf
|
||||
local/logging.conf
|
||||
default/logging.conf
|
||||
|
||||
The current working directory is set to *<app-root>* before the logging configuration file is loaded. Hence, paths
|
||||
in the logging configuration file are relative to *<app-root>*. The current directory is reset before return.
|
||||
|
||||
You may short circuit the search for a logging configuration file by providing an alternative file location in
|
||||
`path`. Logging configuration files must be in `ConfigParser format`_.
|
||||
|
||||
#Arguments:
|
||||
|
||||
:param logger_name: Logger name
|
||||
:type logger_name: bytes, unicode
|
||||
|
||||
:param filename: Location of an alternative logging configuration file or `None`.
|
||||
:type filename: bytes, unicode or NoneType
|
||||
|
||||
:returns: The named logger and the location of the logging configuration file loaded.
|
||||
:rtype: tuple
|
||||
|
||||
.. _ConfigParser format: https://docs.python.org/2/library/logging.config.html#configuration-file-format
|
||||
|
||||
"""
|
||||
if filename is None:
|
||||
if logger_name is None:
|
||||
probing_paths = [path.join('local', 'logging.conf'), path.join('default', 'logging.conf')]
|
||||
else:
|
||||
probing_paths = [
|
||||
path.join('local', logger_name + '.logging.conf'),
|
||||
path.join('default', logger_name + '.logging.conf'),
|
||||
path.join('local', 'logging.conf'),
|
||||
path.join('default', 'logging.conf')]
|
||||
for relative_path in probing_paths:
|
||||
configuration_file = path.join(app_root, relative_path)
|
||||
if path.exists(configuration_file):
|
||||
filename = configuration_file
|
||||
break
|
||||
elif not path.isabs(filename):
|
||||
found = False
|
||||
for conf in 'local', 'default':
|
||||
configuration_file = path.join(app_root, conf, filename)
|
||||
if path.exists(configuration_file):
|
||||
filename = configuration_file
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
raise ValueError('Logging configuration file "{}" not found in local or default directory'.format(filename))
|
||||
elif not path.exists(filename):
|
||||
raise ValueError('Logging configuration file "{}" not found'.format(filename))
|
||||
|
||||
if filename is not None:
|
||||
global _current_logging_configuration_file
|
||||
filename = path.realpath(filename)
|
||||
|
||||
if filename != _current_logging_configuration_file:
|
||||
working_directory = getcwdu()
|
||||
chdir(app_root)
|
||||
try:
|
||||
fileConfig(filename, {'SPLUNK_HOME': splunk_home})
|
||||
finally:
|
||||
chdir(working_directory)
|
||||
_current_logging_configuration_file = filename
|
||||
|
||||
if len(root.handlers) == 0:
|
||||
root.addHandler(StreamHandler())
|
||||
|
||||
return None if logger_name is None else getLogger(logger_name), filename
|
||||
|
||||
|
||||
_current_logging_configuration_file = None
|
||||
|
||||
splunk_home = path.abspath(path.join(getcwdu(), environ.get('SPLUNK_HOME', '')))
|
||||
app_file = getattr(sys.modules['__main__'], '__file__', sys.executable)
|
||||
app_root = path.dirname(path.abspath(path.dirname(app_file)))
|
||||
|
||||
splunklib_logger, logging_configuration = configure_logging('splunklib')
|
||||
|
||||
|
||||
__all__ = ['app_file', 'app_root', 'logging_configuration', 'splunk_home', 'splunklib_logger']
|
||||
@@ -0,0 +1,142 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from itertools import imap
|
||||
|
||||
from .decorators import ConfigurationSetting
|
||||
from .search_command import SearchCommand
|
||||
|
||||
|
||||
class EventingCommand(SearchCommand):
|
||||
""" Applies a transformation to search results as they travel through the events pipeline.
|
||||
|
||||
Eventing commands typically filter, group, order, and/or or augment event records. Examples of eventing commands
|
||||
from Splunk's built-in command set include sort_, dedup_, and cluster_. Each execution of an eventing command
|
||||
should produce a set of event records that is independently usable by downstream processors.
|
||||
|
||||
.. _sort: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Sort
|
||||
.. _dedup: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Dedup
|
||||
.. _cluster: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Cluster
|
||||
|
||||
EventingCommand configuration
|
||||
==============================
|
||||
|
||||
You can configure your command for operation under Search Command Protocol (SCP) version 1 or 2. SCP 2 requires
|
||||
Splunk 6.3 or later.
|
||||
|
||||
"""
|
||||
# region Methods
|
||||
|
||||
def transform(self, records):
|
||||
""" Generator function that processes and yields event records to the Splunk events pipeline.
|
||||
|
||||
You must override this method.
|
||||
|
||||
"""
|
||||
raise NotImplementedError('EventingCommand.transform(self, records)')
|
||||
|
||||
def _execute(self, ifile, process):
|
||||
SearchCommand._execute(self, ifile, self.transform)
|
||||
|
||||
# endregion
|
||||
|
||||
class ConfigurationSettings(SearchCommand.ConfigurationSettings):
|
||||
""" Represents the configuration settings that apply to a :class:`EventingCommand`.
|
||||
|
||||
"""
|
||||
# region SCP v1/v2 properties
|
||||
|
||||
required_fields = ConfigurationSetting(doc='''
|
||||
List of required fields for this search which back-propagates to the generating search.
|
||||
|
||||
Setting this value enables selected fields mode under SCP 2. Under SCP 1 you must also specify
|
||||
:code:`clear_required_fields=True` to enable selected fields mode. To explicitly select all fields,
|
||||
specify a value of :const:`['*']`. No error is generated if a specified field is missing.
|
||||
|
||||
Default: :const:`None`, which implicitly selects all fields.
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v1 properties
|
||||
|
||||
clear_required_fields = ConfigurationSetting(doc='''
|
||||
:const:`True`, if required_fields represent the *only* fields required.
|
||||
|
||||
If :const:`False`, required_fields are additive to any fields that may be required by subsequent commands.
|
||||
In most cases, :const:`False` is appropriate for eventing commands.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
''')
|
||||
|
||||
retainsevents = ConfigurationSetting(readonly=True, value=True, doc='''
|
||||
:const:`True`, if the command retains events the way the sort/dedup/cluster commands do.
|
||||
|
||||
If :const:`False`, the command transforms events the way the stats command does.
|
||||
|
||||
Fixed: :const:`True`
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v2 properties
|
||||
|
||||
maxinputs = ConfigurationSetting(doc='''
|
||||
Specifies the maximum number of events that can be passed to the command for each invocation.
|
||||
|
||||
This limit cannot exceed the value of `maxresultrows` as defined in limits.conf_. Under SCP 1 you must
|
||||
specify this value in commands.conf_.
|
||||
|
||||
Default: The value of `maxresultrows`.
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
.. _limits.conf: http://docs.splunk.com/Documentation/Splunk/latest/admin/Limitsconf
|
||||
|
||||
''')
|
||||
|
||||
type = ConfigurationSetting(readonly=True, value='eventing', doc='''
|
||||
Command type
|
||||
|
||||
Fixed: :const:`'eventing'`.
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region Methods
|
||||
|
||||
@classmethod
|
||||
def fix_up(cls, command):
|
||||
""" Verifies :code:`command` class structure.
|
||||
|
||||
"""
|
||||
if command.transform == EventingCommand.transform:
|
||||
raise AttributeError('No EventingCommand.transform override')
|
||||
SearchCommand.ConfigurationSettings.fix_up(command)
|
||||
|
||||
def iteritems(self):
|
||||
iteritems = SearchCommand.ConfigurationSettings.iteritems(self)
|
||||
return imap(lambda (name, value): (name, 'events' if name == 'type' else value), iteritems)
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,227 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from signal import signal, CTRL_BREAK_EVENT, SIGBREAK, SIGINT, SIGTERM
|
||||
from subprocess import Popen
|
||||
import atexit
|
||||
|
||||
from . import splunklib_logger as logger
|
||||
|
||||
# P1 [ ] TODO: Add ExternalSearchCommand class documentation
|
||||
|
||||
|
||||
class ExternalSearchCommand(object):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, path, argv=None, environ=None):
|
||||
|
||||
if not isinstance(path, (bytes, unicode)):
|
||||
raise ValueError('Expected a string value for path, not {}'.format(repr(path)))
|
||||
|
||||
self._logger = getLogger(self.__class__.__name__)
|
||||
self._path = unicode(path)
|
||||
self._argv = None
|
||||
self._environ = None
|
||||
|
||||
self.argv = argv
|
||||
self.environ = environ
|
||||
|
||||
# region Properties
|
||||
|
||||
@property
|
||||
def argv(self):
|
||||
return getattr(self, '_argv')
|
||||
|
||||
@argv.setter
|
||||
def argv(self, value):
|
||||
if not (value is None or isinstance(value, (list, tuple))):
|
||||
raise ValueError('Expected a list, tuple or value of None for argv, not {}'.format(repr(value)))
|
||||
self._argv = value
|
||||
|
||||
@property
|
||||
def environ(self):
|
||||
return getattr(self, '_environ')
|
||||
|
||||
@environ.setter
|
||||
def environ(self, value):
|
||||
if not (value is None or isinstance(value, dict)):
|
||||
raise ValueError('Expected a dictionary value for environ, not {}'.format(repr(value)))
|
||||
self._environ = value
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
return self._logger
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
# endregion
|
||||
|
||||
# region Methods
|
||||
|
||||
def execute(self):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if self._argv is None:
|
||||
self._argv = os.path.splitext(os.path.basename(self._path))[0]
|
||||
self._execute(self._path, self._argv, self._environ)
|
||||
except:
|
||||
error_type, error, tb = sys.exc_info()
|
||||
message = 'Command execution failed: ' + unicode(error)
|
||||
self._logger.error(message + '\nTraceback:\n' + ''.join(traceback.format_tb(tb)))
|
||||
sys.exit(1)
|
||||
|
||||
if sys.platform == 'win32':
|
||||
|
||||
@staticmethod
|
||||
def _execute(path, argv=None, environ=None):
|
||||
""" Executes an external search command.
|
||||
|
||||
:param path: Path to the external search command.
|
||||
:type path: unicode
|
||||
|
||||
:param argv: Argument list.
|
||||
:type argv: list or tuple
|
||||
The arguments to the child process should start with the name of the command being run, but this is not
|
||||
enforced. A value of :const:`None` specifies that the base name of path name :param:`path` should be used.
|
||||
|
||||
:param environ: A mapping which is used to define the environment variables for the new process.
|
||||
:type environ: dict or None.
|
||||
This mapping is used instead of the current process’s environment. A value of :const:`None` specifies that
|
||||
the :data:`os.environ` mapping should be used.
|
||||
|
||||
:return: None
|
||||
|
||||
"""
|
||||
search_path = os.getenv('PATH') if environ is None else environ.get('PATH')
|
||||
found = ExternalSearchCommand._search_path(path, search_path)
|
||||
|
||||
if found is None:
|
||||
raise ValueError('Cannot find command on path: {}'.format(path))
|
||||
|
||||
path = found
|
||||
logger.debug('starting command="%s", arguments=%s', path, argv)
|
||||
|
||||
def terminate(signal_number, frame):
|
||||
sys.exit('External search command is terminating on receipt of signal={}.'.format(signal_number))
|
||||
|
||||
def terminate_child():
|
||||
if p.pid is not None and p.returncode is None:
|
||||
logger.debug('terminating command="%s", arguments=%d, pid=%d', path, argv, p.pid)
|
||||
os.kill(p.pid, CTRL_BREAK_EVENT)
|
||||
|
||||
p = Popen(argv, executable=path, env=environ, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
|
||||
atexit.register(terminate_child)
|
||||
signal(SIGBREAK, terminate)
|
||||
signal(SIGINT, terminate)
|
||||
signal(SIGTERM, terminate)
|
||||
|
||||
logger.debug('started command="%s", arguments=%s, pid=%d', path, argv, p.pid)
|
||||
p.wait()
|
||||
|
||||
logger.debug('finished command="%s", arguments=%s, pid=%d, returncode=%d', path, argv, p.pid, p.returncode)
|
||||
|
||||
if p.returncode != 0:
|
||||
sys.exit(p.returncode)
|
||||
|
||||
@staticmethod
|
||||
def _search_path(executable, paths):
|
||||
""" Locates an executable program file.
|
||||
|
||||
:param executable: The name of the executable program to locate.
|
||||
:type executable: unicode
|
||||
|
||||
:param paths: A list of one or more directory paths where executable programs are located.
|
||||
:type paths: unicode
|
||||
|
||||
:return:
|
||||
:rtype: Path to the executable program located or :const:`None`.
|
||||
|
||||
"""
|
||||
directory, filename = os.path.split(executable)
|
||||
extension = os.path.splitext(filename)[1].upper()
|
||||
executable_extensions = ExternalSearchCommand._executable_extensions
|
||||
|
||||
if directory:
|
||||
if len(extension) and extension in executable_extensions:
|
||||
return None
|
||||
for extension in executable_extensions:
|
||||
path = executable + extension
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
if not paths:
|
||||
return None
|
||||
|
||||
directories = [directory for directory in paths.split(';') if len(directory)]
|
||||
|
||||
if len(directories) == 0:
|
||||
return None
|
||||
|
||||
if len(extension) and extension in executable_extensions:
|
||||
for directory in directories:
|
||||
path = os.path.join(directory, executable)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
for directory in directories:
|
||||
path_without_extension = os.path.join(directory, executable)
|
||||
for extension in executable_extensions:
|
||||
path = path_without_extension + extension
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
_executable_extensions = ('.COM', '.EXE')
|
||||
else:
|
||||
@staticmethod
|
||||
def _execute(path, argv, environ):
|
||||
if environ is None:
|
||||
os.execvp(path, argv)
|
||||
else:
|
||||
os.execvpe(path, argv, environ)
|
||||
return
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
def execute(path, argv=None, environ=None, command_class=ExternalSearchCommand):
|
||||
"""
|
||||
:param path:
|
||||
:type path: basestring
|
||||
:param argv:
|
||||
:type: argv: list, tuple, or None
|
||||
:param environ:
|
||||
:type environ: dict
|
||||
:param command_class: External search command class to instantiate and execute.
|
||||
:type command_class: type
|
||||
:return:
|
||||
:rtype: None
|
||||
"""
|
||||
assert issubclass(command_class, ExternalSearchCommand)
|
||||
command_class(path, argv, environ).execute()
|
||||
@@ -0,0 +1,320 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright © 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from .decorators import ConfigurationSetting
|
||||
from .search_command import SearchCommand
|
||||
|
||||
from itertools import imap, ifilter
|
||||
|
||||
# P1 [O] TODO: Discuss generates_timeorder in the class-level documentation for GeneratingCommand
|
||||
|
||||
|
||||
class GeneratingCommand(SearchCommand):
|
||||
""" Generates events based on command arguments.
|
||||
|
||||
Generating commands receive no input and must be the first command on a pipeline. There are three pipelines:
|
||||
streams, events, and reports. The streams pipeline generates or processes time-ordered event records on an
|
||||
indexer or search head.
|
||||
|
||||
Streaming commands filter, modify, or augment event records and can be applied to subsets of index data in a
|
||||
parallel manner. An example of a streaming command from Splunk's built-in command set is rex_ which extracts and
|
||||
adds fields to event records at search time. Records that pass through the streams pipeline move on to the events
|
||||
pipeline.
|
||||
|
||||
The events pipeline generates or processes records on a search head. Eventing commands typically filter, group,
|
||||
order, or augment event records. Examples of eventing commands from Splunk's built-in command set include sort_,
|
||||
dedup_, and cluster_. Each execution of an eventing command should produce a set of event records that is
|
||||
independently usable by downstream processors. Records that pass through the events pipeline move on to the reports
|
||||
pipeline.
|
||||
|
||||
The reports pipeline also runs on a search head, but yields data structures for presentation, not event records.
|
||||
Examples of streaming from Splunk's built-in command set include chart_, stats_, and contingency_.
|
||||
|
||||
GeneratingCommand configuration
|
||||
===============================
|
||||
|
||||
Configure your generating command based on the pipeline that it targets. How you configure your command depends on
|
||||
the Search Command Protocol (SCP) version.
|
||||
|
||||
+----------+-------------------------------------+--------------------------------------------+
|
||||
| Pipeline | SCP 1 | SCP 2 |
|
||||
+==========+=====================================+============================================+
|
||||
| streams | streaming=True[,local=[True|False]] | type='streaming'[,distributed=[true|false] |
|
||||
+----------+-------------------------------------+--------------------------------------------+
|
||||
| events | retainsevents=True, streaming=False | type='eventing' |
|
||||
+----------+-------------------------------------+--------------------------------------------+
|
||||
| reports | streaming=False | type='reporting' |
|
||||
+----------+-------------------------------------+--------------------------------------------+
|
||||
|
||||
Only streaming commands may be distributed to indexers. By default generating commands are configured to run
|
||||
locally in the streams pipeline and will run under either SCP 1 or SCP 2.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@Configuration()
|
||||
class StreamingGeneratingCommand(GeneratingCommand)
|
||||
...
|
||||
|
||||
How you configure your command to run on a different pipeline or in a distributed fashion depends on what SCP
|
||||
protocol versions you wish to support. You must be sure to configure your command consistently for each protocol,
|
||||
if you wish to support both protocol versions correctly.
|
||||
|
||||
.. _chart: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Chart
|
||||
.. _cluster: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Cluster
|
||||
.. _contingency: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Contingency
|
||||
.. _dedup: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Dedup
|
||||
.. _rex: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Rex
|
||||
.. _sort: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Sort
|
||||
.. _stats: http://docs.splunk.com/Documentation/Splunk/latest/SearchReference/Stats
|
||||
|
||||
Distributed Generating command
|
||||
==============================
|
||||
|
||||
Commands configured like this will run as the first command on search heads and/or indexers on the streams pipeline.
|
||||
|
||||
+----------+---------------------------------------------------+---------------------------------------------------+
|
||||
| Pipeline | SCP 1 | SCP 2 |
|
||||
+==========+===================================================+===================================================+
|
||||
| streams | 1. Add this line to your command's stanza in | 1. Add this configuration setting to your code: |
|
||||
| | | |
|
||||
| | default/commands.conf. | .. code-block:: python |
|
||||
| | .. code-block:: python | @Configuration(distributed=True) |
|
||||
| | local = false | class SomeCommand(GeneratingCommand) |
|
||||
| | | ... |
|
||||
| | 2. Restart splunk | |
|
||||
| | | 2. You are good to go; no need to restart Splunk |
|
||||
+----------+---------------------------------------------------+---------------------------------------------------+
|
||||
|
||||
Eventing Generating command
|
||||
===========================
|
||||
|
||||
Generating commands configured like this will run as the first command on a search head on the events pipeline.
|
||||
|
||||
+----------+---------------------------------------------------+---------------------------------------------------+
|
||||
| Pipeline | SCP 1 | SCP 2 |
|
||||
+==========+===================================================+===================================================+
|
||||
| events | You have a choice. Add these configuration | Add this configuration setting to your command |
|
||||
| | settings to your command class: | setting to your command class: |
|
||||
| | | |
|
||||
| | .. code-block:: python | .. code-block:: python |
|
||||
| | @Configuration( | @Configuration(type='eventing') |
|
||||
| | retainsevents=True, streaming=False) | class SomeCommand(GeneratingCommand) |
|
||||
| | class SomeCommand(GeneratingCommand) | ... |
|
||||
| | ... | |
|
||||
| | | |
|
||||
| | Or add these lines to default/commands.conf: | |
|
||||
| | | |
|
||||
| | .. code-block:: | |
|
||||
| | retains events = true | |
|
||||
| | streaming = false | |
|
||||
+----------+---------------------------------------------------+---------------------------------------------------+
|
||||
|
||||
Configure your command class like this, if you wish to support both protocols:
|
||||
|
||||
.. code-block:: python
|
||||
@Configuration(type='eventing', retainsevents=True, streaming=False)
|
||||
class SomeCommand(GeneratingCommand)
|
||||
...
|
||||
|
||||
You might also consider adding these lines to commands.conf instead of adding them to your command class:
|
||||
|
||||
.. code-block:: python
|
||||
retains events = false
|
||||
streaming = false
|
||||
|
||||
Reporting Generating command
|
||||
============================
|
||||
|
||||
Commands configured like this will run as the first command on a search head on the reports pipeline.
|
||||
|
||||
+----------+---------------------------------------------------+---------------------------------------------------+
|
||||
| Pipeline | SCP 1 | SCP 2 |
|
||||
+==========+===================================================+===================================================+
|
||||
| events | You have a choice. Add these configuration | Add this configuration setting to your command |
|
||||
| | settings to your command class: | setting to your command class: |
|
||||
| | | |
|
||||
| | .. code-block:: python | .. code-block:: python |
|
||||
| | @Configuration(retainsevents=False) | @Configuration(type='reporting') |
|
||||
| | class SomeCommand(GeneratingCommand) | class SomeCommand(GeneratingCommand) |
|
||||
| | ... | ... |
|
||||
| | | |
|
||||
| | Or add this lines to default/commands.conf: | |
|
||||
| | | |
|
||||
| | .. code-block:: | |
|
||||
| | retains events = false | |
|
||||
| | streaming = false | |
|
||||
+----------+---------------------------------------------------+---------------------------------------------------+
|
||||
|
||||
Configure your command class like this, if you wish to support both protocols:
|
||||
|
||||
.. code-block:: python
|
||||
@Configuration(type='reporting', streaming=False)
|
||||
class SomeCommand(GeneratingCommand)
|
||||
...
|
||||
|
||||
You might also consider adding these lines to commands.conf instead of adding them to your command class:
|
||||
|
||||
.. code-block:: python
|
||||
retains events = false
|
||||
streaming = false
|
||||
|
||||
"""
|
||||
# region Methods
|
||||
|
||||
def generate(self):
|
||||
""" A generator that yields records to the Splunk processing pipeline
|
||||
|
||||
You must override this method.
|
||||
|
||||
"""
|
||||
raise NotImplementedError('GeneratingCommand.generate(self)')
|
||||
|
||||
def _execute(self, ifile, process):
|
||||
""" Execution loop
|
||||
|
||||
:param ifile: Input file object. Unused.
|
||||
:type ifile: file
|
||||
|
||||
:return: `None`.
|
||||
|
||||
"""
|
||||
self._record_writer.write_records(self.generate())
|
||||
self.finish()
|
||||
|
||||
# endregion
|
||||
|
||||
# region Types
|
||||
|
||||
class ConfigurationSettings(SearchCommand.ConfigurationSettings):
|
||||
""" Represents the configuration settings for a :code:`GeneratingCommand` class.
|
||||
|
||||
"""
|
||||
# region SCP v1/v2 Properties
|
||||
|
||||
generating = ConfigurationSetting(readonly=True, value=True, doc='''
|
||||
Tells Splunk that this command generates events, but does not process inputs.
|
||||
|
||||
Generating commands must appear at the front of the search pipeline identified by :meth:`type`.
|
||||
|
||||
Fixed: :const:`True`
|
||||
|
||||
Supported by: SCP 1, SCP 2
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v1 Properties
|
||||
|
||||
generates_timeorder = ConfigurationSetting(doc='''
|
||||
:const:`True`, if the command generates new events.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
local = ConfigurationSetting(doc='''
|
||||
:const:`True`, if the command should run locally on the search head.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
retainsevents = ConfigurationSetting(doc='''
|
||||
:const:`True`, if the command retains events the way the sort, dedup, and cluster commands do, or whether it
|
||||
transforms them the way the stats command does.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
streaming = ConfigurationSetting(doc='''
|
||||
:const:`True`, if the command is streamable.
|
||||
|
||||
Default: :const:`True`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v2 Properties
|
||||
|
||||
distributed = ConfigurationSetting(value=False, doc='''
|
||||
True, if this command should be distributed to indexers.
|
||||
|
||||
This value is ignored unless :meth:`type` is equal to :const:`streaming`. It is only this command type that
|
||||
may be distributed.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
''')
|
||||
|
||||
type = ConfigurationSetting(value='streaming', doc='''
|
||||
A command type name.
|
||||
|
||||
==================== ======================================================================================
|
||||
Value Description
|
||||
-------------------- --------------------------------------------------------------------------------------
|
||||
:const:`'eventing'` Runs as the first command in the Splunk events pipeline. Cannot be distributed.
|
||||
:const:`'reporting'` Runs as the first command in the Splunk reports pipeline. Cannot be distributed.
|
||||
:const:`'streaming'` Runs as the first command in the Splunk streams pipeline. May be distributed.
|
||||
==================== ======================================================================================
|
||||
|
||||
Default: :const:`'streaming'`
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region Methods
|
||||
|
||||
@classmethod
|
||||
def fix_up(cls, command):
|
||||
""" Verifies :code:`command` class structure.
|
||||
|
||||
"""
|
||||
if command.generate == GeneratingCommand.generate:
|
||||
raise AttributeError('No GeneratingCommand.generate override')
|
||||
|
||||
def iteritems(self):
|
||||
iteritems = SearchCommand.ConfigurationSettings.iteritems(self)
|
||||
version = self.command.protocol_version
|
||||
if version == 2:
|
||||
iteritems = ifilter(lambda (name, value): name != 'distributed', iteritems)
|
||||
if self.distributed and self.type == 'streaming':
|
||||
iteritems = imap(
|
||||
lambda (name, value): (name, 'stateful') if name == 'type' else (name, value), iteritems)
|
||||
return iteritems
|
||||
|
||||
pass
|
||||
# endregion
|
||||
|
||||
pass
|
||||
# endregion
|
||||
@@ -0,0 +1,786 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright © 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from collections import deque, namedtuple
|
||||
try:
|
||||
from collections import OrderedDict # must be python 2.7
|
||||
except ImportError:
|
||||
from ..ordereddict import OrderedDict
|
||||
from cStringIO import StringIO
|
||||
from itertools import chain, imap
|
||||
from json import JSONDecoder, JSONEncoder
|
||||
from json.encoder import encode_basestring_ascii as json_encode_string
|
||||
from urllib import unquote
|
||||
|
||||
import csv
|
||||
import gzip
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from . import environment
|
||||
|
||||
csv.field_size_limit(10485760) # The default value is 128KB; upping to 10MB. See SPL-12117 for background on this issue
|
||||
|
||||
if sys.platform == 'win32':
|
||||
# Work around the fact that on Windows '\n' is mapped to '\r\n'. The typical solution is to simply open files in
|
||||
# binary mode, but stdout is already open, thus this hack. 'CPython' and 'PyPy' work differently. We assume that
|
||||
# all other Python implementations are compatible with 'CPython'. This might or might not be a valid assumption.
|
||||
from platform import python_implementation
|
||||
implementation = python_implementation()
|
||||
fileno = sys.stdout.fileno()
|
||||
if implementation == 'PyPy':
|
||||
sys.stdout = os.fdopen(fileno, 'wb', 0)
|
||||
else:
|
||||
from msvcrt import setmode
|
||||
setmode(fileno, os.O_BINARY)
|
||||
|
||||
|
||||
class CommandLineParser(object):
|
||||
""" Parses the arguments to a search command.
|
||||
|
||||
A search command line is described by the following syntax.
|
||||
|
||||
**Syntax**::
|
||||
|
||||
command = command-name *[wsp option] *[wsp [dquote] field-name [dquote]]
|
||||
command-name = alpha *( alpha / digit )
|
||||
option = option-name [wsp] "=" [wsp] option-value
|
||||
option-name = alpha *( alpha / digit / "_" )
|
||||
option-value = word / quoted-string
|
||||
word = 1*( %01-%08 / %0B / %0C / %0E-1F / %21 / %23-%FF ) ; Any character but DQUOTE and WSP
|
||||
quoted-string = dquote *( word / wsp / "\" dquote / dquote dquote ) dquote
|
||||
field-name = ( "_" / alpha ) *( alpha / digit / "_" / "." / "-" )
|
||||
|
||||
**Note:**
|
||||
|
||||
This syntax is constrained to an 8-bit character set.
|
||||
|
||||
**Note:**
|
||||
|
||||
This syntax does not show that `field-name` values may be comma-separated when in fact they can be. This is
|
||||
because Splunk strips commas from the command line. A custom search command will never see them.
|
||||
|
||||
**Example:**
|
||||
|
||||
countmatches fieldname = word_count pattern = \w+ some_text_field
|
||||
|
||||
Option names are mapped to properties in the targeted ``SearchCommand``. It is the responsibility of the property
|
||||
setters to validate the values they receive. Property setters may also produce side effects. For example,
|
||||
setting the built-in `log_level` immediately changes the `log_level`.
|
||||
|
||||
"""
|
||||
@classmethod
|
||||
def parse(cls, command, argv):
|
||||
""" Splits an argument list into an options dictionary and a fieldname
|
||||
list.
|
||||
|
||||
The argument list, `argv`, must be of the form::
|
||||
|
||||
*[option]... *[<field-name>]
|
||||
|
||||
Options are validated and assigned to items in `command.options`. Field names are validated and stored in the
|
||||
list of `command.fieldnames`.
|
||||
|
||||
#Arguments:
|
||||
|
||||
:param command: Search command instance.
|
||||
:type command: ``SearchCommand``
|
||||
:param argv: List of search command arguments.
|
||||
:type argv: ``list``
|
||||
:return: ``None``
|
||||
|
||||
#Exceptions:
|
||||
|
||||
``SyntaxError``: Argument list is incorrectly formed.
|
||||
``ValueError``: Unrecognized option/field name, or an illegal field value.
|
||||
|
||||
"""
|
||||
debug = environment.splunklib_logger.debug
|
||||
command_class = type(command).__name__
|
||||
|
||||
# Prepare
|
||||
|
||||
debug('Parsing %s command line: %r', command_class, argv)
|
||||
command.fieldnames = None
|
||||
command.options.reset()
|
||||
argv = ' '.join(argv)
|
||||
|
||||
command_args = cls._arguments_re.match(argv)
|
||||
|
||||
if command_args is None:
|
||||
raise SyntaxError('Syntax error: {}'.format(argv))
|
||||
|
||||
# Parse options
|
||||
|
||||
for option in cls._options_re.finditer(command_args.group('options')):
|
||||
name, value = option.group('name'), option.group('value')
|
||||
if name not in command.options:
|
||||
raise ValueError(
|
||||
'Unrecognized {} command option: {}={}'.format(command.name, name, json_encode_string(value)))
|
||||
command.options[name].value = cls.unquote(value)
|
||||
|
||||
missing = command.options.get_missing()
|
||||
|
||||
if missing is not None:
|
||||
if len(missing) > 1:
|
||||
raise ValueError(
|
||||
'Values for these {} command options are required: {}'.format(command.name, ', '.join(missing)))
|
||||
raise ValueError('A value for {} command option {} is required'.format(command.name, missing[0]))
|
||||
|
||||
# Parse field names
|
||||
|
||||
fieldnames = command_args.group('fieldnames')
|
||||
|
||||
if fieldnames is None:
|
||||
command.fieldnames = []
|
||||
else:
|
||||
command.fieldnames = [cls.unquote(value.group(0)) for value in cls._fieldnames_re.finditer(fieldnames)]
|
||||
|
||||
debug(' %s: %s', command_class, command)
|
||||
|
||||
@classmethod
|
||||
def unquote(cls, string):
|
||||
""" Removes quotes from a quoted string.
|
||||
|
||||
Splunk search command quote rules are applied. The enclosing double-quotes, if present, are removed. Escaped
|
||||
double-quotes ('\"' or '""') are replaced by a single double-quote ('"').
|
||||
|
||||
**NOTE**
|
||||
|
||||
We are not using a json.JSONDecoder because Splunk quote rules are different than JSON quote rules. A
|
||||
json.JSONDecoder does not recognize a pair of double-quotes ('""') as an escaped quote ('"') and will
|
||||
decode single-quoted strings ("'") in addition to double-quoted ('"') strings.
|
||||
|
||||
"""
|
||||
if len(string) == 0:
|
||||
return ''
|
||||
|
||||
if string[0] == '"':
|
||||
if len(string) == 1 or string[-1] != '"':
|
||||
raise SyntaxError('Poorly formed string literal: ' + string)
|
||||
string = string[1:-1]
|
||||
|
||||
if len(string) == 0:
|
||||
return ''
|
||||
|
||||
def replace(match):
|
||||
value = match.group(0)
|
||||
if value == '""':
|
||||
return '"'
|
||||
if len(value) < 2:
|
||||
raise SyntaxError('Poorly formed string literal: ' + string)
|
||||
return value[1]
|
||||
|
||||
result = re.sub(cls._escaped_character_re, replace, string)
|
||||
return result
|
||||
|
||||
# region Class variables
|
||||
|
||||
_arguments_re = re.compile(r"""
|
||||
^\s*
|
||||
(?P<options> # Match a leading set of name/value pairs
|
||||
(?:
|
||||
(?:(?=\w)[^\d]\w*) # name
|
||||
\s*=\s* # =
|
||||
(?:"(?:\\.|""|[^"])*"|(?:\\.|[^\s"])+)\s* # value
|
||||
)*
|
||||
)\s*
|
||||
(?P<fieldnames> # Match a trailing set of field names
|
||||
(?:
|
||||
(?:"(?:\\.|""|[^"])*"|(?:\\.|[^\s"])+)\s*
|
||||
)*
|
||||
)\s*$
|
||||
""", re.VERBOSE | re.UNICODE)
|
||||
|
||||
_escaped_character_re = re.compile(r'(\\.|""|[\\"])')
|
||||
|
||||
_fieldnames_re = re.compile(r"""("(?:\\.|""|[^"])+"|(?:\\.|[^\s"])+)""")
|
||||
|
||||
_options_re = re.compile(r"""
|
||||
# Captures a set of name/value pairs when used with re.finditer
|
||||
(?P<name>(?:(?=\w)[^\d]\w*)) # name
|
||||
\s*=\s* # =
|
||||
(?P<value>"(?:\\.|""|[^"])*"|(?:\\.|[^\s"])+) # value
|
||||
""", re.VERBOSE | re.UNICODE)
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
class ConfigurationSettingsType(type):
|
||||
""" Metaclass for constructing ConfigurationSettings classes.
|
||||
|
||||
Instances of :class:`ConfigurationSettingsType` construct :class:`ConfigurationSettings` classes from classes from
|
||||
a base :class:`ConfigurationSettings` class and a dictionary of configuration settings. The settings in the
|
||||
dictionary are validated against the settings in the base class. You cannot add settings, you can only change their
|
||||
backing-field values and you cannot modify settings without backing-field values. These are considered fixed
|
||||
configuration setting values.
|
||||
|
||||
This is an internal class used in two places:
|
||||
|
||||
+ :meth:`decorators.Configuration.__call__`
|
||||
|
||||
Adds a ConfigurationSettings attribute to a :class:`SearchCommand` class.
|
||||
|
||||
+ :meth:`reporting_command.ReportingCommand.fix_up`
|
||||
|
||||
Adds a ConfigurationSettings attribute to a :meth:`ReportingCommand.map` method, if there is one.
|
||||
|
||||
"""
|
||||
def __new__(mcs, module, name, bases):
|
||||
mcs = super(ConfigurationSettingsType, mcs).__new__(mcs, name, bases, {})
|
||||
return mcs
|
||||
|
||||
def __init__(cls, module, name, bases):
|
||||
|
||||
super(ConfigurationSettingsType, cls).__init__(name, bases, None)
|
||||
cls.__module__ = module
|
||||
|
||||
@staticmethod
|
||||
def validate_configuration_setting(specification, name, value):
|
||||
if not isinstance(value, specification.type):
|
||||
if isinstance(specification.type, type):
|
||||
type_names = specification.type.__name__
|
||||
else:
|
||||
type_names = ', '.join(imap(lambda t: t.__name__, specification.type))
|
||||
raise ValueError('Expected {} value, not {}={}'.format(type_names, name, repr(value)))
|
||||
if specification.constraint and not specification.constraint(value):
|
||||
raise ValueError('Illegal value: {}={}'.format(name, repr(value)))
|
||||
return value
|
||||
|
||||
specification = namedtuple(
|
||||
b'ConfigurationSettingSpecification', (
|
||||
b'type',
|
||||
b'constraint',
|
||||
b'supporting_protocols'))
|
||||
|
||||
# P1 [ ] TODO: Review ConfigurationSettingsType.specification_matrix for completeness and correctness
|
||||
|
||||
specification_matrix = {
|
||||
'clear_required_fields': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1]),
|
||||
'distributed': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[2]),
|
||||
'generates_timeorder': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1]),
|
||||
'generating': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1, 2]),
|
||||
'local': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1]),
|
||||
'maxinputs': specification(
|
||||
type=int,
|
||||
constraint=lambda value: 0 <= value <= sys.maxint,
|
||||
supporting_protocols=[2]),
|
||||
'overrides_timeorder': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1]),
|
||||
'required_fields': specification(
|
||||
type=(list, set, tuple),
|
||||
constraint=None,
|
||||
supporting_protocols=[1, 2]),
|
||||
'requires_preop': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1]),
|
||||
'retainsevents': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1]),
|
||||
'run_in_preview': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[2]),
|
||||
'streaming': specification(
|
||||
type=bool,
|
||||
constraint=None,
|
||||
supporting_protocols=[1]),
|
||||
'streaming_preop': specification(
|
||||
type=(bytes, unicode),
|
||||
constraint=None,
|
||||
supporting_protocols=[1, 2]),
|
||||
'type': specification(
|
||||
type=(bytes, unicode),
|
||||
constraint=lambda value: value in ('eventing', 'reporting', 'streaming'),
|
||||
supporting_protocols=[2])}
|
||||
|
||||
|
||||
class CsvDialect(csv.Dialect):
|
||||
""" Describes the properties of Splunk CSV streams """
|
||||
delimiter = b','
|
||||
quotechar = b'"'
|
||||
doublequote = True
|
||||
skipinitialspace = False
|
||||
lineterminator = b'\r\n'
|
||||
quoting = csv.QUOTE_MINIMAL
|
||||
|
||||
|
||||
class InputHeader(dict):
|
||||
""" Represents a Splunk input header as a collection of name/value pairs.
|
||||
|
||||
"""
|
||||
def __str__(self):
|
||||
return '\n'.join([name + ':' + value for name, value in self.iteritems()])
|
||||
|
||||
def read(self, ifile):
|
||||
""" Reads an input header from an input file.
|
||||
|
||||
The input header is read as a sequence of *<name>***:***<value>* pairs separated by a newline. The end of the
|
||||
input header is signalled by an empty line or an end-of-file.
|
||||
|
||||
:param ifile: File-like object that supports iteration over lines.
|
||||
|
||||
"""
|
||||
name, value = None, None
|
||||
|
||||
for line in ifile:
|
||||
if line == '\n':
|
||||
break
|
||||
item = line.split(':', 1)
|
||||
if len(item) == 2:
|
||||
# start of a new item
|
||||
if name is not None:
|
||||
self[name] = value[:-1] # value sans trailing newline
|
||||
name, value = item[0], unquote(item[1])
|
||||
elif name is not None:
|
||||
# continuation of the current item
|
||||
value += unquote(line)
|
||||
|
||||
if name is not None: self[name] = value[:-1] if value[-1] == '\n' else value
|
||||
|
||||
|
||||
Message = namedtuple(b'Message', (b'type', b'text'))
|
||||
|
||||
|
||||
class MetadataDecoder(JSONDecoder):
|
||||
|
||||
def __init__(self):
|
||||
JSONDecoder.__init__(self, object_hook=self._object_hook)
|
||||
|
||||
@staticmethod
|
||||
def _object_hook(dictionary):
|
||||
|
||||
object_view = ObjectView(dictionary)
|
||||
stack = deque()
|
||||
stack.append((None, None, dictionary))
|
||||
|
||||
while len(stack):
|
||||
instance, member_name, dictionary = stack.popleft()
|
||||
|
||||
for name, value in dictionary.iteritems():
|
||||
if isinstance(value, dict):
|
||||
stack.append((dictionary, name, value))
|
||||
|
||||
if instance is not None:
|
||||
instance[member_name] = ObjectView(dictionary)
|
||||
|
||||
return object_view
|
||||
|
||||
|
||||
class MetadataEncoder(JSONEncoder):
|
||||
|
||||
def __init__(self):
|
||||
JSONEncoder.__init__(self, separators=MetadataEncoder._separators)
|
||||
|
||||
def default(self, o):
|
||||
return o.__dict__ if isinstance(o, ObjectView) else JSONEncoder.default(self, o)
|
||||
|
||||
_separators = (',', ':')
|
||||
|
||||
|
||||
class ObjectView(object):
|
||||
|
||||
def __init__(self, dictionary):
|
||||
self.__dict__ = dictionary
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
|
||||
class Recorder(object):
|
||||
|
||||
def __init__(self, path, f):
|
||||
self._recording = gzip.open(path + '.gz', 'wb')
|
||||
self._file = f
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._file, name)
|
||||
|
||||
def __iter__(self):
|
||||
for line in self._file:
|
||||
self._recording.write(line)
|
||||
self._recording.flush()
|
||||
yield line
|
||||
|
||||
def read(self, size=None):
|
||||
value = self._file.read() if size is None else self._file.read(size)
|
||||
self._recording.write(value)
|
||||
self._recording.flush()
|
||||
return value
|
||||
|
||||
def readline(self, size=None):
|
||||
value = self._file.readline() if size is None else self._file.readline(size)
|
||||
if len(value) > 0:
|
||||
self._recording.write(value)
|
||||
self._recording.flush()
|
||||
return value
|
||||
|
||||
def record(self, *args):
|
||||
for arg in args:
|
||||
self._recording.write(arg)
|
||||
|
||||
def write(self, text):
|
||||
self._recording.write(text)
|
||||
self._file.write(text)
|
||||
self._recording.flush()
|
||||
|
||||
|
||||
class RecordWriter(object):
|
||||
|
||||
def __init__(self, ofile, maxresultrows=None):
|
||||
self._maxresultrows = 50000 if maxresultrows is None else maxresultrows
|
||||
|
||||
self._ofile = ofile
|
||||
self._fieldnames = None
|
||||
self._buffer = StringIO()
|
||||
|
||||
self._writer = csv.writer(self._buffer, dialect=CsvDialect)
|
||||
self._writerow = self._writer.writerow
|
||||
self._finished = False
|
||||
self._flushed = False
|
||||
|
||||
self._inspector = OrderedDict()
|
||||
self._chunk_count = 0
|
||||
self._record_count = 0
|
||||
self._total_record_count = 0L
|
||||
|
||||
@property
|
||||
def is_flushed(self):
|
||||
return self._flushed
|
||||
|
||||
@is_flushed.setter
|
||||
def is_flushed(self, value):
|
||||
self._flushed = True if value else False
|
||||
|
||||
@property
|
||||
def ofile(self):
|
||||
return self._ofile
|
||||
|
||||
@ofile.setter
|
||||
def ofile(self, value):
|
||||
self._ofile = value
|
||||
|
||||
def flush(self, finished=None, partial=None):
|
||||
assert finished is None or isinstance(finished, bool)
|
||||
assert partial is None or isinstance(partial, bool)
|
||||
assert not (finished is None and partial is None)
|
||||
assert finished is None or partial is None
|
||||
self._ensure_validity()
|
||||
|
||||
def write_message(self, message_type, message_text, *args, **kwargs):
|
||||
self._ensure_validity()
|
||||
self._inspector.setdefault('messages', []).append((message_type, message_text.format(*args, **kwargs)))
|
||||
|
||||
def write_record(self, record):
|
||||
self._ensure_validity()
|
||||
self._write_record(record)
|
||||
|
||||
def write_records(self, records):
|
||||
self._ensure_validity()
|
||||
write_record = self._write_record
|
||||
for record in records:
|
||||
write_record(record)
|
||||
|
||||
def _clear(self):
|
||||
self._buffer.reset()
|
||||
self._buffer.truncate()
|
||||
self._inspector.clear()
|
||||
self._record_count = 0
|
||||
self._flushed = False
|
||||
|
||||
def _ensure_validity(self):
|
||||
if self._finished is True:
|
||||
assert self._record_count == 0 and len(self._inspector) == 0
|
||||
raise RuntimeError('I/O operation on closed record writer')
|
||||
|
||||
def _write_record(self, record):
|
||||
|
||||
fieldnames = self._fieldnames
|
||||
|
||||
if fieldnames is None:
|
||||
self._fieldnames = fieldnames = record.keys()
|
||||
value_list = imap(lambda fn: unicode(fn).encode('utf-8'), fieldnames)
|
||||
value_list = imap(lambda fn: (fn, b'__mv_' + fn), value_list)
|
||||
self._writerow(list(chain.from_iterable(value_list)))
|
||||
|
||||
get_value = record.get
|
||||
values = []
|
||||
|
||||
for fieldname in fieldnames:
|
||||
value = get_value(fieldname, None)
|
||||
|
||||
if value is None:
|
||||
values += (None, None)
|
||||
continue
|
||||
|
||||
value_t = type(value)
|
||||
|
||||
if issubclass(value_t, (list, tuple)):
|
||||
|
||||
if len(value) == 0:
|
||||
values += (None, None)
|
||||
continue
|
||||
|
||||
if len(value) > 1:
|
||||
value_list = value
|
||||
sv = b''
|
||||
mv = b'$'
|
||||
|
||||
for value in value_list:
|
||||
|
||||
if value is None:
|
||||
sv += b'\n'
|
||||
mv += b'$;$'
|
||||
continue
|
||||
|
||||
value_t = type(value)
|
||||
|
||||
if value_t is not bytes:
|
||||
|
||||
if value_t is bool:
|
||||
value = str(value.real)
|
||||
elif value_t is unicode:
|
||||
value = value.encode('utf-8', errors='backslashreplace')
|
||||
elif value_t is int or value_t is long or value_t is float or value_t is complex:
|
||||
value = str(value)
|
||||
elif issubclass(value_t, (dict, list, tuple)):
|
||||
value = str(''.join(RecordWriter._iterencode_json(value, 0)))
|
||||
else:
|
||||
value = repr(value).encode('utf-8', errors='backslashreplace')
|
||||
|
||||
sv += value + b'\n'
|
||||
mv += value.replace(b'$', b'$$') + b'$;$'
|
||||
|
||||
values += (sv[:-1], mv[:-2])
|
||||
continue
|
||||
|
||||
value = value[0]
|
||||
value_t = type(value)
|
||||
|
||||
if value_t is bool:
|
||||
values += (str(value.real), None)
|
||||
continue
|
||||
|
||||
if value_t is bytes:
|
||||
values += (value, None)
|
||||
continue
|
||||
|
||||
if value_t is unicode:
|
||||
values += (value.encode('utf-8', errors='backslashreplace'), None)
|
||||
continue
|
||||
|
||||
if value_t is int or value_t is long or value_t is float or value_t is complex:
|
||||
values += (str(value), None)
|
||||
continue
|
||||
|
||||
if issubclass(value_t, dict):
|
||||
values += (str(''.join(RecordWriter._iterencode_json(value, 0))), None)
|
||||
continue
|
||||
|
||||
values += (repr(value).encode('utf-8', errors='backslashreplace'), None)
|
||||
|
||||
self._writerow(values)
|
||||
self._record_count += 1
|
||||
|
||||
if self._record_count >= self._maxresultrows:
|
||||
self.flush(partial=True)
|
||||
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
from _json import make_encoder
|
||||
except ImportError:
|
||||
# We may be running under PyPy 2.5 which does not include the _json module
|
||||
_iterencode_json = JSONEncoder(separators=(',', ':')).iterencode
|
||||
else:
|
||||
# Creating _iterencode_json this way yields a two-fold performance improvement on Python 2.7.9 and 2.7.10
|
||||
from json.encoder import encode_basestring_ascii
|
||||
|
||||
@staticmethod
|
||||
def _default(o):
|
||||
raise TypeError(repr(o) + ' is not JSON serializable')
|
||||
|
||||
_iterencode_json = make_encoder(
|
||||
{}, # markers (for detecting circular references)
|
||||
_default, # object_encoder
|
||||
encode_basestring_ascii, # string_encoder
|
||||
None, # indent
|
||||
':', ',', # separators
|
||||
False, # sort_keys
|
||||
False, # skip_keys
|
||||
True # allow_nan
|
||||
)
|
||||
|
||||
del make_encoder
|
||||
|
||||
|
||||
class RecordWriterV1(RecordWriter):
|
||||
|
||||
def flush(self, finished=None, partial=None):
|
||||
|
||||
RecordWriter.flush(self, finished, partial) # validates arguments and the state of this instance
|
||||
|
||||
if self._record_count > 0 or (self._chunk_count == 0 and 'messages' in self._inspector):
|
||||
|
||||
messages = self._inspector.get('messages')
|
||||
write = self._ofile.write
|
||||
|
||||
if self._chunk_count == 0:
|
||||
|
||||
# Messages are written to the messages header when we write the first chunk of data
|
||||
# Guarantee: These messages are displayed by splunkweb and the job inspector
|
||||
|
||||
if messages is not None:
|
||||
|
||||
message_level = RecordWriterV1._message_level.get
|
||||
|
||||
for level, text in messages:
|
||||
write(message_level(level, level))
|
||||
write('=')
|
||||
write(text)
|
||||
write('\r\n')
|
||||
|
||||
write('\r\n')
|
||||
|
||||
elif messages is not None:
|
||||
|
||||
# Messages are written to the messages header when we write subsequent chunks of data
|
||||
# Guarantee: These messages are displayed by splunkweb and the job inspector, if and only if the
|
||||
# command is configured with
|
||||
#
|
||||
# stderr_dest = message
|
||||
#
|
||||
# stderr_dest is a static configuration setting. This means that it can only be set in commands.conf.
|
||||
# It cannot be set in code.
|
||||
|
||||
stderr = sys.stderr
|
||||
|
||||
for level, text in messages:
|
||||
print(level, text, file=stderr)
|
||||
|
||||
write(self._buffer.getvalue())
|
||||
self._clear()
|
||||
self._chunk_count += 1
|
||||
self._total_record_count += self._record_count
|
||||
|
||||
self._finished = finished is True
|
||||
|
||||
_message_level = {
|
||||
'DEBUG': 'debug_message',
|
||||
'ERROR': 'error_message',
|
||||
'FATAL': 'error_message',
|
||||
'INFO': 'info_message',
|
||||
'WARN': 'warn_message'
|
||||
}
|
||||
|
||||
|
||||
class RecordWriterV2(RecordWriter):
|
||||
|
||||
def flush(self, finished=None, partial=None):
|
||||
|
||||
RecordWriter.flush(self, finished, partial) # validates arguments and the state of this instance
|
||||
inspector = self._inspector
|
||||
|
||||
if self._flushed is False:
|
||||
|
||||
self._total_record_count += self._record_count
|
||||
self._chunk_count += 1
|
||||
|
||||
# TODO: DVPL-6448: splunklib.searchcommands | Add support for partial: true when it is implemented in
|
||||
# ChunkedExternProcessor (See SPL-103525)
|
||||
#
|
||||
# We will need to replace the following block of code with this block:
|
||||
#
|
||||
# metadata = [
|
||||
# ('inspector', self._inspector if len(self._inspector) else None),
|
||||
# ('finished', finished),
|
||||
# ('partial', partial)]
|
||||
|
||||
if len(inspector) == 0:
|
||||
inspector = None
|
||||
|
||||
if partial is True:
|
||||
finished = False
|
||||
|
||||
metadata = [item for item in ('inspector', inspector), ('finished', finished)]
|
||||
self._write_chunk(metadata, self._buffer.getvalue())
|
||||
self._clear()
|
||||
|
||||
elif finished is True:
|
||||
self._write_chunk((('finished', True),), '')
|
||||
|
||||
self._finished = finished is True
|
||||
|
||||
def write_metadata(self, configuration):
|
||||
self._ensure_validity()
|
||||
|
||||
metadata = chain(configuration.iteritems(), (('inspector', self._inspector if self._inspector else None),))
|
||||
self._write_chunk(metadata, '')
|
||||
self._ofile.write('\n')
|
||||
self._clear()
|
||||
|
||||
def write_metric(self, name, value):
|
||||
self._ensure_validity()
|
||||
self._inspector['metric.' + name] = value
|
||||
|
||||
def _clear(self):
|
||||
RecordWriter._clear(self)
|
||||
self._fieldnames = None
|
||||
|
||||
def _write_chunk(self, metadata, body):
|
||||
|
||||
if metadata:
|
||||
metadata = str(''.join(self._iterencode_json(dict([(n, v) for n, v in metadata if v is not None]), 0)))
|
||||
metadata_length = len(metadata)
|
||||
else:
|
||||
metadata_length = 0
|
||||
|
||||
body_length = len(body)
|
||||
|
||||
if not (metadata_length > 0 or body_length > 0):
|
||||
return
|
||||
|
||||
start_line = b'chunked 1.0,' + bytes(metadata_length) + b',' + bytes(body_length) + b'\n'
|
||||
write = self._ofile.write
|
||||
write(start_line)
|
||||
write(metadata)
|
||||
write(body)
|
||||
self._ofile.flush()
|
||||
self._flushed = False
|
||||
@@ -0,0 +1,280 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright © 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from .internals import ConfigurationSettingsType, json_encode_string
|
||||
from .decorators import ConfigurationSetting, Option
|
||||
from .streaming_command import StreamingCommand
|
||||
from .search_command import SearchCommand
|
||||
from .validators import Set
|
||||
|
||||
|
||||
class ReportingCommand(SearchCommand):
|
||||
""" Processes search result records and generates a reporting data structure.
|
||||
|
||||
Reporting search commands run as either reduce or map/reduce operations. The reduce part runs on a search head and
|
||||
is responsible for processing a single chunk of search results to produce the command's reporting data structure.
|
||||
The map part is called a streaming preop. It feeds the reduce part with partial results and by default runs on the
|
||||
search head and/or one or more indexers.
|
||||
|
||||
You must implement a :meth:`reduce` method as a generator function that iterates over a set of event records and
|
||||
yields a reporting data structure. You may implement a :meth:`map` method as a generator function that iterates
|
||||
over a set of event records and yields :class:`dict` or :class:`list(dict)` instances.
|
||||
|
||||
ReportingCommand configuration
|
||||
==============================
|
||||
|
||||
Configure the :meth:`map` operation using a Configuration decorator on your :meth:`map` method. Configure it like
|
||||
you would a :class:`StreamingCommand`. Configure the :meth:`reduce` operation using a Configuration decorator on
|
||||
your :meth:`ReportingCommand` class.
|
||||
|
||||
You can configure your command for operation under Search Command Protocol (SCP) version 1 or 2. SCP 2 requires
|
||||
Splunk 6.3 or later.
|
||||
|
||||
"""
|
||||
# region Special methods
|
||||
|
||||
def __init__(self):
|
||||
SearchCommand.__init__(self)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Options
|
||||
|
||||
phase = Option(doc='''
|
||||
**Syntax:** phase=[map|reduce]
|
||||
|
||||
**Description:** Identifies the phase of the current map-reduce operation.
|
||||
|
||||
''', default='reduce', validate=Set('map', 'reduce'))
|
||||
|
||||
# endregion
|
||||
|
||||
# region Methods
|
||||
|
||||
def map(self, records):
|
||||
""" Override this method to compute partial results.
|
||||
|
||||
:param records:
|
||||
:type records:
|
||||
|
||||
You must override this method, if :code:`requires_preop=True`.
|
||||
|
||||
"""
|
||||
return NotImplemented
|
||||
|
||||
def prepare(self):
|
||||
|
||||
phase = self.phase
|
||||
|
||||
if phase == 'map':
|
||||
# noinspection PyUnresolvedReferences
|
||||
self._configuration = self.map.ConfigurationSettings(self)
|
||||
return
|
||||
|
||||
if phase == 'reduce':
|
||||
streaming_preop = chain((self.name, 'phase="map"', str(self._options)), self.fieldnames)
|
||||
self._configuration.streaming_preop = ' '.join(streaming_preop)
|
||||
return
|
||||
|
||||
raise RuntimeError('Unrecognized reporting command phase: {}'.format(json_encode_string(unicode(phase))))
|
||||
|
||||
def reduce(self, records):
|
||||
""" Override this method to produce a reporting data structure.
|
||||
|
||||
You must override this method.
|
||||
|
||||
"""
|
||||
raise NotImplementedError('reduce(self, records)')
|
||||
|
||||
def _execute(self, ifile, process):
|
||||
SearchCommand._execute(self, ifile, getattr(self, self.phase))
|
||||
|
||||
# endregion
|
||||
|
||||
# region Types
|
||||
|
||||
class ConfigurationSettings(SearchCommand.ConfigurationSettings):
|
||||
""" Represents the configuration settings for a :code:`ReportingCommand`.
|
||||
|
||||
"""
|
||||
# region SCP v1/v2 Properties
|
||||
|
||||
required_fields = ConfigurationSetting(doc='''
|
||||
List of required fields for this search which back-propagates to the generating search.
|
||||
|
||||
Setting this value enables selected fields mode under SCP 2. Under SCP 1 you must also specify
|
||||
:code:`clear_required_fields=True` to enable selected fields mode. To explicitly select all fields,
|
||||
specify a value of :const:`['*']`. No error is generated if a specified field is missing.
|
||||
|
||||
Default: :const:`None`, which implicitly selects all fields.
|
||||
|
||||
Supported by: SCP 1, SCP 2
|
||||
|
||||
''')
|
||||
|
||||
requires_preop = ConfigurationSetting(doc='''
|
||||
Indicates whether :meth:`ReportingCommand.map` is required for proper command execution.
|
||||
|
||||
If :const:`True`, :meth:`ReportingCommand.map` is guaranteed to be called. If :const:`False`, Splunk
|
||||
considers it to be an optimization that may be skipped.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 1, SCP 2
|
||||
|
||||
''')
|
||||
|
||||
streaming_preop = ConfigurationSetting(doc='''
|
||||
Denotes the requested streaming preop search string.
|
||||
|
||||
Computed.
|
||||
|
||||
Supported by: SCP 1, SCP 2
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v1 Properties
|
||||
|
||||
clear_required_fields = ConfigurationSetting(doc='''
|
||||
:const:`True`, if required_fields represent the *only* fields required.
|
||||
|
||||
If :const:`False`, required_fields are additive to any fields that may be required by subsequent commands.
|
||||
In most cases, :const:`True` is appropriate for reporting commands.
|
||||
|
||||
Default: :const:`True`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
retainsevents = ConfigurationSetting(readonly=True, value=False, doc='''
|
||||
Signals that :meth:`ReportingCommand.reduce` transforms _raw events to produce a reporting data structure.
|
||||
|
||||
Fixed: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
streaming = ConfigurationSetting(readonly=True, value=False, doc='''
|
||||
Signals that :meth:`ReportingCommand.reduce` runs on the search head.
|
||||
|
||||
Fixed: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v2 Properties
|
||||
|
||||
maxinputs = ConfigurationSetting(doc='''
|
||||
Specifies the maximum number of events that can be passed to the command for each invocation.
|
||||
|
||||
This limit cannot exceed the value of `maxresultrows` in limits.conf_. Under SCP 1 you must specify this
|
||||
value in commands.conf_.
|
||||
|
||||
Default: The value of `maxresultrows`.
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
.. _limits.conf: http://docs.splunk.com/Documentation/Splunk/latest/admin/Limitsconf
|
||||
|
||||
''')
|
||||
|
||||
run_in_preview = ConfigurationSetting(doc='''
|
||||
:const:`True`, if this command should be run to generate results for preview; not wait for final output.
|
||||
|
||||
This may be important for commands that have side effects (e.g., outputlookup).
|
||||
|
||||
Default: :const:`True`
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
''')
|
||||
|
||||
type = ConfigurationSetting(readonly=True, value='reporting', doc='''
|
||||
Command type name.
|
||||
|
||||
Fixed: :const:`'reporting'`.
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region Methods
|
||||
|
||||
@classmethod
|
||||
def fix_up(cls, command):
|
||||
""" Verifies :code:`command` class structure and configures the :code:`command.map` method.
|
||||
|
||||
Verifies that :code:`command` derives from :class:`ReportingCommand` and overrides
|
||||
:code:`ReportingCommand.reduce`. It then configures :code:`command.reduce`, if an overriding implementation
|
||||
of :code:`ReportingCommand.reduce` has been provided.
|
||||
|
||||
:param command: :code:`ReportingCommand` class
|
||||
|
||||
Exceptions:
|
||||
|
||||
:code:`TypeError` :code:`command` class is not derived from :code:`ReportingCommand`
|
||||
:code:`AttributeError` No :code:`ReportingCommand.reduce` override
|
||||
|
||||
"""
|
||||
if not issubclass(command, ReportingCommand):
|
||||
raise TypeError('{} is not a ReportingCommand'.format( command))
|
||||
|
||||
if command.reduce == ReportingCommand.reduce:
|
||||
raise AttributeError('No ReportingCommand.reduce override')
|
||||
|
||||
if command.map == ReportingCommand.map:
|
||||
cls._requires_preop = False
|
||||
return
|
||||
|
||||
f = vars(command)[b'map'] # Function backing the map method
|
||||
|
||||
# EXPLANATION OF PREVIOUS STATEMENT: There is no way to add custom attributes to methods. See [Why does
|
||||
# setattr fail on a method](http://stackoverflow.com/questions/7891277/why-does-setattr-fail-on-a-bound-method) for a discussion of this issue.
|
||||
|
||||
try:
|
||||
settings = f._settings
|
||||
except AttributeError:
|
||||
f.ConfigurationSettings = StreamingCommand.ConfigurationSettings
|
||||
return
|
||||
|
||||
# Create new StreamingCommand.ConfigurationSettings class
|
||||
|
||||
module = command.__module__ + b'.' + command.__name__ + b'.map'
|
||||
name = b'ConfigurationSettings'
|
||||
bases = (StreamingCommand.ConfigurationSettings,)
|
||||
|
||||
f.ConfigurationSettings = ConfigurationSettingsType(module, name, bases)
|
||||
ConfigurationSetting.fix_up(f.ConfigurationSettings, settings)
|
||||
del f._settings
|
||||
|
||||
pass
|
||||
# endregion
|
||||
|
||||
pass
|
||||
# endregion
|
||||
@@ -0,0 +1,188 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from itertools import ifilter, imap
|
||||
|
||||
from .decorators import ConfigurationSetting
|
||||
from .search_command import SearchCommand
|
||||
|
||||
|
||||
class StreamingCommand(SearchCommand):
|
||||
""" Applies a transformation to search results as they travel through the streams pipeline.
|
||||
|
||||
Streaming commands typically filter, augment, or update, search result records. Splunk will send them in batches of
|
||||
up to 50,000 records. Hence, a search command must be prepared to be invoked many times during the course of
|
||||
pipeline processing. Each invocation should produce a set of results independently usable by downstream processors.
|
||||
|
||||
By default Splunk may choose to run a streaming command locally on a search head and/or remotely on one or more
|
||||
indexers concurrently. The size and frequency of the search result batches sent to the command will vary based
|
||||
on scheduling considerations.
|
||||
|
||||
StreamingCommand configuration
|
||||
==============================
|
||||
|
||||
You can configure your command for operation under Search Command Protocol (SCP) version 1 or 2. SCP 2 requires
|
||||
Splunk 6.3 or later.
|
||||
|
||||
"""
|
||||
# region Methods
|
||||
|
||||
def stream(self, records):
|
||||
""" Generator function that processes and yields event records to the Splunk stream pipeline.
|
||||
|
||||
You must override this method.
|
||||
|
||||
"""
|
||||
raise NotImplementedError('StreamingCommand.stream(self, records)')
|
||||
|
||||
def _execute(self, ifile, process):
|
||||
SearchCommand._execute(self, ifile, self.stream)
|
||||
|
||||
# endregion
|
||||
|
||||
class ConfigurationSettings(SearchCommand.ConfigurationSettings):
|
||||
""" Represents the configuration settings that apply to a :class:`StreamingCommand`.
|
||||
|
||||
"""
|
||||
# region SCP v1/v2 properties
|
||||
|
||||
required_fields = ConfigurationSetting(doc='''
|
||||
List of required fields for this search which back-propagates to the generating search.
|
||||
|
||||
Setting this value enables selected fields mode under SCP 2. Under SCP 1 you must also specify
|
||||
:code:`clear_required_fields=True` to enable selected fields mode. To explicitly select all fields,
|
||||
specify a value of :const:`['*']`. No error is generated if a specified field is missing.
|
||||
|
||||
Default: :const:`None`, which implicitly selects all fields.
|
||||
|
||||
Supported by: SCP 1, SCP 2
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v1 properties
|
||||
|
||||
clear_required_fields = ConfigurationSetting(doc='''
|
||||
:const:`True`, if required_fields represent the *only* fields required.
|
||||
|
||||
If :const:`False`, required_fields are additive to any fields that may be required by subsequent commands.
|
||||
In most cases, :const:`False` is appropriate for streaming commands.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
local = ConfigurationSetting(doc='''
|
||||
:const:`True`, if the command should run locally on the search head.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
overrides_timeorder = ConfigurationSetting(doc='''
|
||||
:const:`True`, if the command changes the order of events with respect to time.
|
||||
|
||||
Default: :const:`False`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
streaming = ConfigurationSetting(readonly=True, value=True, doc='''
|
||||
Specifies that the command is streamable.
|
||||
|
||||
Fixed: :const:`True`
|
||||
|
||||
Supported by: SCP 1
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region SCP v2 Properties
|
||||
|
||||
distributed = ConfigurationSetting(value=True, doc='''
|
||||
:const:`True`, if this command should be distributed to indexers.
|
||||
|
||||
Under SCP 1 you must either specify `local = False` or include this line in commands.conf_, if this command
|
||||
should be distributed to indexers.
|
||||
|
||||
..code:
|
||||
local = true
|
||||
|
||||
Default: :const:`True`
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
.. commands.conf_: http://docs.splunk.com/Documentation/Splunk/latest/Admin/Commandsconf
|
||||
|
||||
''')
|
||||
|
||||
maxinputs = ConfigurationSetting(doc='''
|
||||
Specifies the maximum number of events that can be passed to the command for each invocation.
|
||||
|
||||
This limit cannot exceed the value of `maxresultrows` in limits.conf. Under SCP 1 you must specify this
|
||||
value in commands.conf_.
|
||||
|
||||
Default: The value of `maxresultrows`.
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
''')
|
||||
|
||||
type = ConfigurationSetting(readonly=True, value='streaming', doc='''
|
||||
Command type name.
|
||||
|
||||
Fixed: :const:`'streaming'`
|
||||
|
||||
Supported by: SCP 2
|
||||
|
||||
''')
|
||||
|
||||
# endregion
|
||||
|
||||
# region Methods
|
||||
|
||||
@classmethod
|
||||
def fix_up(cls, command):
|
||||
""" Verifies :code:`command` class structure.
|
||||
|
||||
"""
|
||||
if command.stream == StreamingCommand.stream:
|
||||
raise AttributeError('No StreamingCommand.stream override')
|
||||
return
|
||||
|
||||
def iteritems(self):
|
||||
iteritems = SearchCommand.ConfigurationSettings.iteritems(self)
|
||||
version = self.command.protocol_version
|
||||
if version == 1:
|
||||
if self.required_fields is None:
|
||||
iteritems = ifilter(lambda (name, value): name != 'clear_required_fields', iteritems)
|
||||
else:
|
||||
iteritems = ifilter(lambda (name, value): name != 'distributed', iteritems)
|
||||
if self.distributed:
|
||||
iteritems = imap(
|
||||
lambda (name, value): (name, 'stateful') if name == 'type' else (name, value), iteritems)
|
||||
return iteritems
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,384 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright 2011-2015 Splunk, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from json.encoder import encode_basestring_ascii as json_encode_string
|
||||
from collections import namedtuple
|
||||
from cStringIO import StringIO
|
||||
from io import open
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class Validator(object):
|
||||
""" Base class for validators that check and format search command options.
|
||||
|
||||
You must inherit from this class and override :code:`Validator.__call__` and
|
||||
:code:`Validator.format`. :code:`Validator.__call__` should convert the
|
||||
value it receives as argument and then return it or raise a
|
||||
:code:`ValueError`, if the value will not convert.
|
||||
|
||||
:code:`Validator.format` should return a human readable version of the value
|
||||
it receives as argument the same way :code:`str` does.
|
||||
|
||||
"""
|
||||
def __call__(self, value):
|
||||
raise NotImplementedError()
|
||||
|
||||
def format(self, value):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Boolean(Validator):
|
||||
""" Validates Boolean option values.
|
||||
|
||||
"""
|
||||
truth_values = {
|
||||
'1': True, '0': False,
|
||||
't': True, 'f': False,
|
||||
'true': True, 'false': False,
|
||||
'y': True, 'n': False,
|
||||
'yes': True, 'no': False
|
||||
}
|
||||
|
||||
def __call__(self, value):
|
||||
if not (value is None or isinstance(value, bool)):
|
||||
value = unicode(value).lower()
|
||||
if value not in Boolean.truth_values:
|
||||
raise ValueError('Unrecognized truth value: {0}'.format(value))
|
||||
value = Boolean.truth_values[value]
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else 't' if value else 'f'
|
||||
|
||||
|
||||
class Code(Validator):
|
||||
""" Validates code option values.
|
||||
|
||||
This validator compiles an option value into a Python code object that can be executed by :func:`exec` or evaluated
|
||||
by :func:`eval`. The value returned is a :func:`namedtuple` with two members: object, the result of compilation, and
|
||||
source, the original option value.
|
||||
|
||||
"""
|
||||
def __init__(self, mode='eval'):
|
||||
"""
|
||||
:param mode: Specifies what kind of code must be compiled; it can be :const:`'exec'`, if source consists of a
|
||||
sequence of statements, :const:`'eval'`, if it consists of a single expression, or :const:`'single'` if it
|
||||
consists of a single interactive statement. In the latter case, expression statements that evaluate to
|
||||
something other than :const:`None` will be printed.
|
||||
:type mode: unicode or bytes
|
||||
|
||||
"""
|
||||
self._mode = mode
|
||||
|
||||
def __call__(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return Code.object(compile(value, 'string', self._mode), unicode(value))
|
||||
except (SyntaxError, TypeError) as error:
|
||||
raise ValueError(error.message)
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else value.source
|
||||
|
||||
object = namedtuple(b'Code', (b'object', 'source'))
|
||||
|
||||
|
||||
class Fieldname(Validator):
|
||||
""" Validates field name option values.
|
||||
|
||||
"""
|
||||
pattern = re.compile(r'''[_.a-zA-Z-][_.a-zA-Z0-9-]*$''')
|
||||
|
||||
def __call__(self, value):
|
||||
if value is not None:
|
||||
value = unicode(value)
|
||||
if Fieldname.pattern.match(value) is None:
|
||||
raise ValueError('Illegal characters in fieldname: {}'.format(value))
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class File(Validator):
|
||||
""" Validates file option values.
|
||||
|
||||
"""
|
||||
def __init__(self, mode='rt', buffering=None, directory=None):
|
||||
self.mode = mode
|
||||
self.buffering = buffering
|
||||
self.directory = File._var_run_splunk if directory is None else directory
|
||||
|
||||
def __call__(self, value):
|
||||
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
path = unicode(value)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.join(self.directory, path)
|
||||
|
||||
try:
|
||||
value = open(path, self.mode) if self.buffering is None else open(path, self.mode, self.buffering)
|
||||
except IOError as error:
|
||||
raise ValueError('Cannot open {0} with mode={1} and buffering={2}: {3}'.format(
|
||||
value, self.mode, self.buffering, error))
|
||||
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else value.name
|
||||
|
||||
_var_run_splunk = os.path.join(
|
||||
os.environ['SPLUNK_HOME'] if 'SPLUNK_HOME' in os.environ else os.getcwdu(), 'var', 'run', 'splunk')
|
||||
|
||||
|
||||
class Integer(Validator):
|
||||
""" Validates integer option values.
|
||||
|
||||
"""
|
||||
def __init__(self, minimum=None, maximum=None):
|
||||
if minimum is not None and maximum is not None:
|
||||
def check_range(value):
|
||||
if not (minimum <= value <= maximum):
|
||||
raise ValueError('Expected integer in the range [{0},{1}], not {2}'.format(minimum, maximum, value))
|
||||
return
|
||||
elif minimum is not None:
|
||||
def check_range(value):
|
||||
if value < minimum:
|
||||
raise ValueError('Expected integer in the range [{0},+∞], not {1}'.format(minimum, value))
|
||||
return
|
||||
elif maximum is not None:
|
||||
def check_range(value):
|
||||
if value > maximum:
|
||||
raise ValueError('Expected integer in the range [-∞,{0}], not {1}'.format(maximum, value))
|
||||
return
|
||||
else:
|
||||
def check_range(value):
|
||||
return
|
||||
|
||||
self.check_range = check_range
|
||||
return
|
||||
|
||||
def __call__(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
value = long(value)
|
||||
except ValueError:
|
||||
raise ValueError('Expected integer value, not {}'.format(json_encode_string(value)))
|
||||
|
||||
self.check_range(value)
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else unicode(long(value))
|
||||
|
||||
|
||||
class Duration(Validator):
|
||||
""" Validates duration option values.
|
||||
|
||||
"""
|
||||
def __call__(self, value):
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
p = value.split(':', 2)
|
||||
result = None
|
||||
_60 = Duration._60
|
||||
_unsigned = Duration._unsigned
|
||||
|
||||
try:
|
||||
if len(p) == 1:
|
||||
result = _unsigned(p[0])
|
||||
if len(p) == 2:
|
||||
result = 60 * _unsigned(p[0]) + _60(p[1])
|
||||
if len(p) == 3:
|
||||
result = 3600 * _unsigned(p[0]) + 60 * _60(p[1]) + _60(p[2])
|
||||
except ValueError:
|
||||
raise ValueError('Invalid duration value: {0}'.format(value))
|
||||
|
||||
return result
|
||||
|
||||
def format(self, value):
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
value = int(value)
|
||||
|
||||
s = value % 60
|
||||
m = value // 60 % 60
|
||||
h = value // (60 * 60)
|
||||
|
||||
return '{0:02d}:{1:02d}:{2:02d}'.format(h, m, s)
|
||||
|
||||
_60 = Integer(0, 59)
|
||||
_unsigned = Integer(0)
|
||||
|
||||
|
||||
class List(Validator):
|
||||
""" Validates a list of strings
|
||||
|
||||
"""
|
||||
class Dialect(csv.Dialect):
|
||||
""" Describes the properties of list option values. """
|
||||
strict = True
|
||||
delimiter = b','
|
||||
quotechar = b'"'
|
||||
doublequote = True
|
||||
lineterminator = b'\n'
|
||||
skipinitialspace = True
|
||||
quoting = csv.QUOTE_MINIMAL
|
||||
|
||||
def __init__(self, validator=None):
|
||||
if not (validator is None or isinstance(validator, Validator)):
|
||||
raise ValueError('Expected a Validator instance or None for validator, not {}', repr(validator))
|
||||
self._validator = validator
|
||||
|
||||
def __call__(self, value):
|
||||
|
||||
if value is None or isinstance(value, list):
|
||||
return value
|
||||
|
||||
try:
|
||||
value = csv.reader([value], self.Dialect).next()
|
||||
except csv.Error as error:
|
||||
raise ValueError(error)
|
||||
|
||||
if self._validator is None:
|
||||
return value
|
||||
|
||||
try:
|
||||
for index, item in enumerate(value):
|
||||
value[index] = self._validator(item)
|
||||
except ValueError as error:
|
||||
raise ValueError('Could not convert item {}: {}'.format(index, error))
|
||||
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
output = StringIO()
|
||||
writer = csv.writer(output, List.Dialect)
|
||||
writer.writerow(value)
|
||||
value = output.getvalue()
|
||||
return value[:-1]
|
||||
|
||||
|
||||
class Map(Validator):
|
||||
""" Validates map option values.
|
||||
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
self.membership = kwargs
|
||||
|
||||
def __call__(self, value):
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
value = unicode(value)
|
||||
|
||||
if value not in self.membership:
|
||||
raise ValueError('Unrecognized value: {0}'.format(value))
|
||||
|
||||
return self.membership[value]
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else self.membership.keys()[self.membership.values().index(value)]
|
||||
|
||||
|
||||
class Match(Validator):
|
||||
""" Validates that a value matches a regular expression pattern.
|
||||
|
||||
"""
|
||||
def __init__(self, name, pattern, flags=0):
|
||||
self.name = unicode(name)
|
||||
self.pattern = re.compile(pattern, flags)
|
||||
|
||||
def __call__(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
value = unicode(value)
|
||||
if self.pattern.match(value) is None:
|
||||
raise ValueError('Expected {}, not {}'.format(self.name, json_encode_string(value)))
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else unicode(value)
|
||||
|
||||
|
||||
class OptionName(Validator):
|
||||
""" Validates option names.
|
||||
|
||||
"""
|
||||
pattern = re.compile(r'''(?=\w)[^\d]\w*$''', re.UNICODE)
|
||||
|
||||
def __call__(self, value):
|
||||
if value is not None:
|
||||
value = unicode(value)
|
||||
if OptionName.pattern.match(value) is None:
|
||||
raise ValueError('Illegal characters in option name: {}'.format(value))
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else unicode(value)
|
||||
|
||||
|
||||
class RegularExpression(Validator):
|
||||
""" Validates regular expression option values.
|
||||
|
||||
"""
|
||||
def __call__(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
value = re.compile(unicode(value))
|
||||
except re.error as error:
|
||||
raise ValueError('{}: {}'.format(unicode(error).capitalize(), value))
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return None if value is None else value.pattern
|
||||
|
||||
|
||||
class Set(Validator):
|
||||
""" Validates set option values.
|
||||
|
||||
"""
|
||||
def __init__(self, *args):
|
||||
self.membership = set(args)
|
||||
|
||||
def __call__(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
value = unicode(value)
|
||||
if value not in self.membership:
|
||||
raise ValueError('Unrecognized value: {}'.format(value))
|
||||
return value
|
||||
|
||||
def format(self, value):
|
||||
return self.__call__(value)
|
||||
|
||||
|
||||
__all__ = ['Boolean', 'Code', 'Duration', 'File', 'Integer', 'List', 'Map', 'RegularExpression', 'Set']
|
||||