Skip to content

Commit 6561dfc

Browse files
committed
Add CI/CD validation workflows and tools
1 parent fa0c846 commit 6561dfc

File tree

16 files changed

+1592
-173
lines changed

16 files changed

+1592
-173
lines changed

.bandit.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Bandit security scanner configuration for Splunk app
2+
# Used by pre-commit hooks
3+
4+
# Exclude test files and certain directories
5+
exclude_dirs:
6+
- /tests/
7+
- /test/
8+
- /.git/
9+
- /local/
10+
11+
# Skipped tests (add test IDs to skip specific checks)
12+
skips:
13+
# B404, B603, B607: subprocess security warnings (common in Splunk scripts)
14+
# - B404
15+
# - B603
16+
# - B607
17+
18+
# Tests to run (leave empty to run all except skipped)
19+
tests: []
20+
21+
# Confidence level for reporting
22+
# Options: LOW, MEDIUM, HIGH
23+
confidence: MEDIUM
24+
25+
# Severity level for reporting
26+
# Options: LOW, MEDIUM, HIGH
27+
severity: MEDIUM

.github/workflows/validate.yml

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
name: Validate Splunk App
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
workflow_dispatch:
9+
10+
jobs:
11+
validate:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: '3.11'
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install splunk-appinspect xmlschema jsonschema pyyaml
27+
28+
- name: Validate JSON files
29+
run: |
30+
echo "Validating app.manifest..."
31+
python -c "import json; json.load(open('app.manifest'))"
32+
echo "✓ app.manifest is valid JSON"
33+
34+
- name: Validate XML files
35+
run: |
36+
echo "Validating XML files..."
37+
for file in $(find default/data/ui -name "*.xml"); do
38+
echo "Checking $file..."
39+
python -c "import xml.etree.ElementTree as ET; ET.parse('$file')"
40+
done
41+
echo "✓ All XML files are well-formed"
42+
43+
- name: Check version consistency
44+
run: |
45+
echo "Checking version consistency..."
46+
MANIFEST_VERSION=$(python -c "import json; print(json.load(open('app.manifest'))['info']['id']['version'])")
47+
APP_CONF_VERSION=$(grep -E "^version\s*=" default/app.conf | sed 's/.*=\s*//' | tr -d ' ')
48+
49+
echo "app.manifest version: $MANIFEST_VERSION"
50+
echo "app.conf version: $APP_CONF_VERSION"
51+
52+
if [ "$MANIFEST_VERSION" != "$APP_CONF_VERSION" ]; then
53+
echo "❌ Version mismatch between app.manifest and app.conf"
54+
exit 1
55+
fi
56+
echo "✓ Versions are consistent"
57+
58+
- name: Check app ID consistency
59+
run: |
60+
echo "Checking app ID consistency..."
61+
MANIFEST_ID=$(python -c "import json; print(json.load(open('app.manifest'))['info']['id']['name'])")
62+
APP_CONF_ID=$(grep -E "^id\s*=" default/app.conf | sed 's/.*=\s*//' | tr -d ' ')
63+
64+
echo "app.manifest id: $MANIFEST_ID"
65+
echo "app.conf id: $APP_CONF_ID"
66+
67+
if [ "$MANIFEST_ID" != "$APP_CONF_ID" ]; then
68+
echo "❌ App ID mismatch between app.manifest and app.conf"
69+
exit 1
70+
fi
71+
echo "✓ App IDs are consistent"
72+
73+
- name: Validate .conf file syntax
74+
run: |
75+
echo "Validating .conf files..."
76+
python << 'EOF'
77+
import re
78+
import sys
79+
from pathlib import Path
80+
81+
errors = []
82+
conf_files = list(Path('default').glob('*.conf'))
83+
84+
for conf_file in conf_files:
85+
print(f"Checking {conf_file}...")
86+
with open(conf_file, 'r', encoding='utf-8') as f:
87+
lines = f.readlines()
88+
in_stanza = False
89+
for i, line in enumerate(lines, 1):
90+
line = line.strip()
91+
if not line or line.startswith('#'):
92+
continue
93+
if line.startswith('[') and line.endswith(']'):
94+
in_stanza = True
95+
continue
96+
if in_stanza and '=' in line:
97+
key, value = line.split('=', 1)
98+
if not key.strip():
99+
errors.append(f"{conf_file}:{i} - Empty key in key=value pair")
100+
101+
if errors:
102+
print("❌ Configuration file errors:")
103+
for error in errors:
104+
print(f" {error}")
105+
sys.exit(1)
106+
else:
107+
print("✓ All .conf files have valid syntax")
108+
EOF
109+
110+
- name: Check for sensitive data
111+
run: |
112+
echo "Scanning for potential sensitive data..."
113+
python << 'EOF'
114+
import re
115+
import sys
116+
from pathlib import Path
117+
118+
patterns = [
119+
(r'password\s*=\s*[^\s]+', 'Potential hardcoded password'),
120+
(r'token\s*=\s*[^\s]+', 'Potential hardcoded token'),
121+
(r'api[_-]?key\s*=\s*[^\s]+', 'Potential hardcoded API key'),
122+
(r'secret\s*=\s*[^\s]+', 'Potential hardcoded secret'),
123+
]
124+
125+
issues = []
126+
for conf_file in Path('default').glob('*.conf'):
127+
with open(conf_file, 'r', encoding='utf-8') as f:
128+
for i, line in enumerate(f, 1):
129+
if line.strip().startswith('#'):
130+
continue
131+
for pattern, message in patterns:
132+
if re.search(pattern, line, re.IGNORECASE):
133+
issues.append(f"{conf_file}:{i} - {message}")
134+
135+
if issues:
136+
print("⚠️ Warning: Potential sensitive data found:")
137+
for issue in issues:
138+
print(f" {issue}")
139+
# Don't fail build, just warn
140+
else:
141+
print("✓ No obvious sensitive data detected")
142+
EOF
143+
144+
- name: Validate lookup definitions
145+
run: |
146+
echo "Validating lookup tables..."
147+
python << 'EOF'
148+
import re
149+
import sys
150+
from pathlib import Path
151+
152+
# Parse transforms.conf for lookup definitions
153+
transforms_file = Path('default/transforms.conf')
154+
lookups_defined = set()
155+
156+
if transforms_file.exists():
157+
with open(transforms_file, 'r') as f:
158+
for line in f:
159+
match = re.match(r'^\[([^\]]+)\]', line.strip())
160+
if match:
161+
lookups_defined.add(match.group(1))
162+
163+
# Check if CSV files exist
164+
lookups_dir = Path('lookups')
165+
errors = []
166+
167+
if lookups_dir.exists():
168+
for lookup_def in lookups_defined:
169+
# Extract CSV filename from definition name (common pattern)
170+
csv_file = lookups_dir / f"{lookup_def}.csv"
171+
if not csv_file.exists():
172+
# Try without prefix if it has one
173+
parts = lookup_def.split('_', 1)
174+
if len(parts) > 1:
175+
csv_file = lookups_dir / f"{parts[1]}.csv"
176+
if not csv_file.exists():
177+
errors.append(f"Lookup '{lookup_def}' defined but CSV not found in lookups/")
178+
179+
if errors:
180+
print("⚠️ Lookup warnings:")
181+
for error in errors:
182+
print(f" {error}")
183+
else:
184+
print(f"✓ Found {len(lookups_defined)} lookup definitions")
185+
EOF
186+
187+
- name: Check required files
188+
run: |
189+
echo "Checking for required files..."
190+
REQUIRED_FILES=(
191+
"app.manifest"
192+
"default/app.conf"
193+
"README.md"
194+
"LICENSE"
195+
"metadata/default.meta"
196+
)
197+
198+
MISSING=()
199+
for file in "${REQUIRED_FILES[@]}"; do
200+
if [ ! -f "$file" ]; then
201+
MISSING+=("$file")
202+
fi
203+
done
204+
205+
if [ ${#MISSING[@]} -ne 0 ]; then
206+
echo "❌ Missing required files:"
207+
printf ' %s\n' "${MISSING[@]}"
208+
exit 1
209+
fi
210+
echo "✓ All required files present"
211+
212+
- name: Run Splunk AppInspect
213+
run: |
214+
echo "Running Splunk AppInspect..."
215+
# Create a temporary package directory
216+
PACKAGE_DIR="caca-package"
217+
mkdir -p "$PACKAGE_DIR"
218+
219+
# Copy app files excluding development files
220+
rsync -av --exclude='.git*' --exclude='local/' --exclude='devnotes/' \
221+
--exclude='*.pyc' --exclude='__pycache__/' --exclude='.DS_Store' \
222+
./ "$PACKAGE_DIR/"
223+
224+
# Run AppInspect
225+
splunk-appinspect inspect "$PACKAGE_DIR" \
226+
--mode precert \
227+
--included-tags cloud \
228+
--output-file appinspect_report.json || true
229+
230+
# Check if report was generated
231+
if [ -f appinspect_report.json ]; then
232+
echo "✓ AppInspect completed - see artifact for results"
233+
else
234+
echo "⚠️ AppInspect report not generated"
235+
fi
236+
237+
- name: Upload AppInspect Report
238+
if: always()
239+
uses: actions/upload-artifact@v4
240+
with:
241+
name: appinspect-report
242+
path: appinspect_report.json
243+
if-no-files-found: ignore
244+
245+
- name: Check AppInspect Results
246+
if: always()
247+
run: |
248+
if [ -f appinspect_report.json ]; then
249+
python << 'EOF'
250+
import json
251+
import sys
252+
253+
with open('appinspect_report.json', 'r') as f:
254+
report = json.load(f)
255+
256+
summary = report.get('summary', {})
257+
failure = summary.get('failure', 0)
258+
error = summary.get('error', 0)
259+
warning = summary.get('warning', 0)
260+
261+
print(f"\nAppInspect Summary:")
262+
print(f" Failures: {failure}")
263+
print(f" Errors: {error}")
264+
print(f" Warnings: {warning}")
265+
266+
if failure > 0 or error > 0:
267+
print("\n❌ AppInspect found failures or errors")
268+
sys.exit(1)
269+
elif warning > 0:
270+
print("\n⚠️ AppInspect found warnings (not failing build)")
271+
else:
272+
print("\n✓ AppInspect passed with no issues")
273+
EOF
274+
else
275+
echo "No AppInspect report to check"
276+
fi
277+
278+
package-validation:
279+
runs-on: ubuntu-latest
280+
281+
steps:
282+
- name: Checkout code
283+
uses: actions/checkout@v4
284+
285+
- name: Check for unwanted files in package
286+
run: |
287+
echo "Checking for files that should not be in package..."
288+
UNWANTED_PATTERNS=(
289+
".git"
290+
".github"
291+
"local/"
292+
"__pycache__"
293+
"*.pyc"
294+
".DS_Store"
295+
".vscode"
296+
"*.swp"
297+
"*.swo"
298+
)
299+
300+
FOUND=()
301+
for pattern in "${UNWANTED_PATTERNS[@]}"; do
302+
if compgen -G "$pattern" > /dev/null 2>&1; then
303+
FOUND+=("$pattern")
304+
fi
305+
done
306+
307+
# Check local/ specifically (it might exist but should be in .gitignore)
308+
if [ -d "local" ] && [ "$(ls -A local)" ]; then
309+
echo "⚠️ Warning: local/ directory exists and is not empty"
310+
fi
311+
312+
if [ ${#FOUND[@]} -ne 0 ]; then
313+
echo "⚠️ Warning: Found unwanted patterns (ensure they're in .gitignore):"
314+
printf ' %s\n' "${FOUND[@]}"
315+
else
316+
echo "✓ No unwanted files found"
317+
fi
318+
319+
- name: Validate .gitignore coverage
320+
run: |
321+
echo "Checking .gitignore coverage..."
322+
SHOULD_IGNORE=(
323+
"local/"
324+
"*.pyc"
325+
"__pycache__/"
326+
".DS_Store"
327+
"*.log"
328+
)
329+
330+
if [ ! -f .gitignore ]; then
331+
echo "⚠️ Warning: No .gitignore file found"
332+
exit 0
333+
fi
334+
335+
MISSING=()
336+
for pattern in "${SHOULD_IGNORE[@]}"; do
337+
if ! grep -q "^${pattern}$" .gitignore; then
338+
MISSING+=("$pattern")
339+
fi
340+
done
341+
342+
if [ ${#MISSING[@]} -ne 0 ]; then
343+
echo "⚠️ Recommended patterns not in .gitignore:"
344+
printf ' %s\n' "${MISSING[@]}"
345+
else
346+
echo "✓ .gitignore covers recommended patterns"
347+
fi

0 commit comments

Comments
 (0)