Skip to content
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
84c9d33
feat: custom seach command feature
hetangmodi-crest Jan 20, 2025
b5989f6
feat: template files for custom search command feature
hetangmodi-crest Jan 20, 2025
039fe0d
tests(unit): add unit test cases
hetangmodi-crest Jan 20, 2025
7432330
tests(smoke): add files for smoke test case
hetangmodi-crest Jan 20, 2025
3802847
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Jan 20, 2025
fda2c7f
doc: update docs, fix lint
hetangmodi-crest Jan 23, 2025
85be57e
ci: fix app_inspect failure
hetangmodi-crest Jan 23, 2025
d73930c
test(smoke): update smoke tests, add better handling
hetangmodi-crest Jan 24, 2025
d4e3f3e
chore: resolve merge conflict
hetangmodi-crest Jan 24, 2025
181da69
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Jan 29, 2025
2e288e7
fix: update schema.json
hetangmodi-crest Feb 4, 2025
d90a475
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Feb 4, 2025
f8d6ce3
chore: merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Feb 7, 2025
1425f9b
feat: generate files using FileGenerator class
hetangmodi-crest Feb 13, 2025
1ef0399
tests: add unit and smoke test cases
hetangmodi-crest Feb 13, 2025
b68d9a6
doc: update documentation
hetangmodi-crest Feb 13, 2025
3951671
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Feb 13, 2025
b3a42b6
docs: updated docs regarding generated conf, xml and html files
srv-rr-github-token Feb 13, 2025
9af4085
ci: fix pipeline failures
hetangmodi-crest Feb 13, 2025
2866cf9
docs: updated docs regarding generated conf, xml and html files
srv-rr-github-token Feb 13, 2025
312ee3c
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Feb 17, 2025
8119835
docs: resolve typos
hetangmodi-crest Feb 19, 2025
afc6bbb
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Feb 19, 2025
5f44b3a
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Mar 4, 2025
4f0a32f
chore: fix typos in source code
hetangmodi-crest Mar 5, 2025
8823338
ci: fix globalConfig everything
hetangmodi-crest Mar 5, 2025
a7ec116
feat: added check for Splunk built-in commands
hetangmodi-crest Mar 10, 2025
9986ec3
chore: merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Mar 10, 2025
e47b2cd
chore: add license headers
hetangmodi-crest Mar 10, 2025
1adadbb
refactor: removed version 1 support
hetangmodi-crest Mar 13, 2025
ff8da5b
tests: updated unit test cases
hetangmodi-crest Mar 13, 2025
24d9860
Merge branch 'develop' into feat/custom-search-command
vtsvetkov-splunk Mar 14, 2025
b9a73e3
Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Mar 17, 2025
80d43e6
chore: Merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Mar 17, 2025
d9c8229
chore: merge branch 'develop' into feat/custom-search-command
hetangmodi-crest Mar 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions docs/custom_search_commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Custom Search Command

Custom search commands are user-defined [SPL](https://docs.splunk.com/Splexicon:SPL) (Splunk Search Processing Language) commands that enable users to add custom functionality to their Splunk searches.

There are two versions of implementing custom search commands:

- Version 1: This uses the InterSplunk module and has been deprecated. (It is not recommended to use the Version 1 protocol.)
- Version 2: Introduced in Splunk 6.3.0, this version is faster, more scalable, and has replaced Version 1.

## Generation of custom search command

A new tag has been introduced in globalConfig (same indent level as of `meta` tag) named `customSearchCommand` where you need to define the configuration for the custom search command.

### Minimal definition

```json
"customSearchCommand": [
{
"commandName": "mycommandname",
"fileName": "mycommandlogic.py",
"commandType": "generating",
"arguments": [
{
"name": "argument_name",
"validate": {
"type": "Fieldname"
},
"required": true
},
{
"name": "argument_two"
}
]
}
]
```

This configuration will generate a template Python file named `mycommandname.py`, which imports logic from the `mycommandlogic.py` file and automatically updates the `commands.conf` file as shown below:

```
[mycommandname]
filename = mycommandname.py
chunked = true
python.version = python3
```

**NOTE:**
If the file specified in the `fileName` field does not exist in the `<YOUR_ADDON/bin>` directory, the build will fail.

### Attributes for `customSearchCommand` tag

| Property | Type | Description |
| ------------------------ | ------ | ------------------------------------ |
| commandName<span class="required-asterisk">\*</span> | string | Name of the custom search command |
| fileName<span class="required-asterisk">\*</span> | string | Name of the Python file which contains logic of custom search command |
| commandType<span class="required-asterisk">\*</span> | string | Specify type of custom search command. Four types of commands are allowed, `streaming`,`generating`,`reporting` and `eventing`. |
| version | number | Specifies the protocol being used (default is 2). |
| arguments<span class="required-asterisk">\*</span> | object | Arguments which can be passed to custom search command. |
| requiredSearchAssistant | boolean | Specifies whether search assistance is required for the custom search command. Default: false. |
| usage | string | Defines the usage of custom search command. It can be one of `public`, `private` and `deprecated`. |
| description | string | Provide description of the custom search command. |
| syntax | string | Provide syntax for custom search command |

To generate a custom search command, the following attributes must be defined in globalConfig: `commandName`, `commandType`, `fileName`, and `arguments`. Based on the provided commandType, UCC will generate a template Python file and integrate the user-defined logic into it.

If `requiredSearchAssistant` is set to True, the `syntax`, `description`, and `usage` attributes are mandatory, as they are essential for generating `searchbnf.conf` file.

**NOTE:**
The user-defined Python file must include specific functions based on the command type:

- For `Generating` command, the Python file must include a `generate` function.
- For `Streaming` command, the Python file must include a `stream` function.
- For `Eventing` command, the Python file must include a `transform` function.
- For `Reporting` command, the Python file must include a `reduce` function, and optionally a `map` function if a streaming pre-operation is required.

## Arguments

| Property | Type | Description |
| --------------------------------------------------------------------- | ------ | ------------------------------------------------------- |
| name<span class="required-asterisk">\*</span> | string | Name of the argument |
| defaultValue | string/number | Default value of the argument. |
| required | string | Specify if the argument is required or not. |
| validate | object | Specify validation for the argument. It can be any of `Integer`, `Float`, `Boolean`, `RegularExpression` or `FieldName`. |

UCC currently supports five types of validations provided by `splunklib` library:

- IntegerValidator
+ you can optionally define `minimum` and `maximum` properties.
- FloatValidator
+ you can optionally define `minimum` and `maximum` properties.
- BooleanValidator
+ no additional properties required.
- RegularExpressionValidator
+ no additional properties required.
- FieldnameValidator
+ no additional properties required.

For more information, refer [splunklib API docs](https://splunk-python-sdk.readthedocs.io/en/latest/searchcommands.html)

For example:

```json
"arguments": [
{
"name": "count",
"required": true,
"validate": {
"type": "Integer",
"minimum": 1,
"maximum": 10
},
"default": 5
},
{
"name": "test",
"required": true,
"validate": {
"type": "Fieldname"
}
},
{
"name": "percent",
"validate": {
"type": "Float",
"minimum": "85.5"
}

}
]

```

## Example

``` json
{
"meta": {...}
"customSearchCommand": [
{
"commandName": "testcommand",
"fileName": "commandlogic.py",
"commandType": "streaming",
"requireSeachAssistant": true,
"version": 2,
"description": "This is a test command",
"syntax": "| testcommand fieldname=<Name of field> pattern=<Valid regex pattern>",
"usage": "public",
"arguments": [
{
"name": "fieldname",
"validate": {
"type": "Fieldname"
}
},
{
"name": "pattern",
"validate": {
"type": "RegularExpression"
},
"required": true
}
]
}
],
"pages": {...}
}
```

Generated python file named `testcommand.py`:

``` python
import sys
import import_declare_test

from splunklib.searchcommands import \
dispatch, StreamingCommand, Configuration, Option, validators
from commandlogic import stream

@Configuration()
class testcommandCommand(StreamingCommand):
"""

##Syntax
This is a test command

##Description
| testcommand fieldname=<Name of field> pattern=<Valid regex pattern>

"""

fieldname = Option(name = "fieldname",require = False, validate = validators.Fieldname(), default = "")
pattern = Option(name = "pattern",require = True, validate = validators.RegularExpression(), default = "")


def stream(self, events):
# Put your event transformation code here
return stream(self,events)

dispatch(testcommandCommand, sys.argv, sys.stdin, sys.stdout, __name__)
```

Generated stanza in `commands.conf` file

```
[testcommand]
filename = testcommand.py
chunked = true
python.version = python3
```

Generated stanza in `searchbnf.conf` file

```
[testcommand]
syntax = | testcommand fieldname=<Name of field> pattern=<Valid regex pattern>
description = This is a test command.
usage = public
```
3 changes: 3 additions & 0 deletions docs/generated_files.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The following table describes the files generated by UCC framework.
| File Name | File Location | File Description |
| ------------ | ------------ | ----------------- |
| app.conf | output/&lt;YOUR_ADDON_NAME&gt;/default | Generates `app.conf` with the details mentioned in globalConfig[meta] |
| commands.conf | output/&lt;YOUR_ADDON_NAME&gt;/default | Generates `commands.conf` for custom commands provided in the globalConfig. |
| serachbnf.conf | output/&lt;YOUR_ADDON_NAME&gt;/default | Generates `searchbnf.conf` for custom search commands provided in the globalConfig. |
| inputs.conf | output/&lt;YOUR_ADDON_NAME&gt;/default | Generates `inputs.conf` and `inputs.conf.spec` file for the services mentioned in globalConfig |
| server.conf | output/&lt;YOUR_ADDON_NAME&gt;/default | Generates `server.conf` for the custom conf files created as per configurations in globalConfig |
| restmap.conf | output/&lt;YOUR_ADDON_NAME&gt;/default | Generates `restmap.conf` for the custom REST handlers that are generated based on configs from globalConfig |
Expand All @@ -22,4 +24,5 @@ The following table describes the files generated by UCC framework.
| inputs.xml | output/&lt;YOUR_ADDON_NAME&gt;/default/data/ui/views | Generates inputs.xml based on inputs configuration present in globalConfig, in `default/data/ui/views/inputs.xml` folder |
| _redirect.xml | output/&lt;YOUR_ADDON_NAME&gt;/default/data/ui/views | Generates ta_name_redirect.xml file, if oauth is mentioned in globalConfig, in `default/data/ui/views/` folder. |
| _.html | output/&lt;YOUR_ADDON_NAME&gt;/default/data/ui/alerts | Generates `alert_name.html` file based on alerts configuration present in globalConfig, in `default/data/ui/alerts` folder. |
| _.py | output/&lt;YOUR_ADDON_NAME&gt;/bin | Generates python files for custom commands provided in the globalConfig. |

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ nav:
- Validators: "entity/validators.md"
- Modify fields On change: "entity/modifyFieldsOnValue.md"

- Custom Search Command: "custom_search_commands.md"
- Table: "table.md"
- Additional packaging: "additional_packaging.md"
- UCC ignore: "uccignore.md"
Expand Down
40 changes: 39 additions & 1 deletion splunk_add_on_ucc_framework/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import colorama as c
import fnmatch
import filecmp

from splunk_add_on_ucc_framework import (
__version__,
exceptions,
Expand Down Expand Up @@ -508,6 +507,44 @@ def generate(
logger.info(
f"Installed add-on requirements into {ucc_lib_target} from {source}"
)
if global_config.has_custom_search_commands():
for command in global_config.custom_search_commands:
file_path = os.path.join(source, "bin", command["fileName"])
if not os.path.isfile(file_path):
logger.error(
f"{command['fileName']} is not present in `{os.path.join(source, 'bin')}` directory. "
"Please ensure the file exists."
)
sys.exit(1)

if (command["requireSeachAssistant"] is False) and (
command.get("description")
or command.get("usage")
or command.get("syntax")
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please check is it possible to have this logic in the schema.json? Or would it be too complicated for the JSON schema to handle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is possible to implement the above logic in JSON schema the reason I think we shouldn't go with schema.json approach is because,

  • JSON Schema validators typically reject invalid data, rather than logging a warning, and in this case I don't think we should stop the build process.
  • JSON Schema library does not support custom error messages directly. It is better to validate and raise warning through above logic.

logger.warning(
"requireSeachAssistant is set to false "
"but atrributes required for 'searchbnf.conf' is defined which is not required."
)
if (command["requireSeachAssistant"] is True) and not (
command.get("description")
and command.get("usage")
and command.get("syntax")
):
logger.error(
"One of the attributes among `description`, `usage`, `syntax` "
" is not been defined in globalConfig. Defined them as requireSeachAssistant is set to True. "
)
sys.exit(1)
if command["version"] == 1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not support version 1? We mention in this PR that it is deprecated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though version 1 is deprecated, we should support it as add-ons like ServiceNow and BMC Remedy uses version 1 for their custom search commands, hence I kept it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove it from UCC, I'll create a Jira for the team to move to the version 2.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, I checked both those add-ons and I can't find version in commands.conf. Is there another way to figure out which version do they use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The determination of version is can be seen based on this description.

Okay, I'll remove the support of version 1 from this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the support of version 1.

command["fileName"] = command["fileName"].replace(".py", "")
if command["commandName"] != command["fileName"]:
logger.error(
f"Filename: {command['fileName']} and CommandName: {command['commandName']}"
" should be same for version 1 of custom search command."
)
sys.exit(1)

generated_files.extend(
begin(
global_config=global_config,
Expand All @@ -518,6 +555,7 @@ def generate(
app_manifest=app_manifest,
addon_version=addon_version,
has_ui=global_config.meta.get("isVisible", True),
custom_search_commands=global_config.custom_search_commands,
)
)
# TODO: all FILES GENERATED object: generated_files, use it for comparison
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from .create_web_conf import WebConf
from .create_account_conf import AccountConf
from .create_settings_conf import SettingsConf
from .create_commands_conf import CommandsConf
from .create_searchbnf_conf import SearchbnfConf

__all__ = [
"ConfGenerator",
Expand All @@ -37,4 +39,6 @@
"InputsConf",
"AccountConf",
"SettingsConf",
"SearchbnfConf",
"CommandsConf",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#
# Copyright 2025 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 typing import Any, Dict, Union

from splunk_add_on_ucc_framework.generators.conf_files import ConfGenerator


class CommandsConf(ConfGenerator):
__description__ = (
"Generates `commands.conf` for custom commands provided in the globalConfig."
)

def _set_attributes(self, **kwargs: Any) -> None:
self.conf_file = "commands.conf"
if self._global_config and self._global_config.has_custom_search_commands():
self.command_names = []
for command in kwargs["custom_search_commands"]:
self.command_names.append(command["commandName"])

def generate_conf(self) -> Union[Dict[str, str], None]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if there is a commands.conf file already in the package folder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the commands.conf is present in the package folder, it would overwrite the one we generated, as it is done for the other conf files (except app.conf). Do we want to merge the generated and source content of the conf files?

if not (
self._global_config and self._global_config.has_custom_search_commands()
):
return None

file_path = self.get_file_output_path(["default", self.conf_file])
self.set_template_and_render(
template_file_path=["conf_files"], file_name="commands.conf.template"
)
rendered_content = self._template.render(
command_names=self.command_names,
)
self.writer(
file_name=self.conf_file,
file_path=file_path,
content=rendered_content,
)
return {self.conf_file: file_path}
Loading
Loading