diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..6e926f3
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,5 @@
+# robopages
+
+## Welcome
+
+Hello and welcome to robopages, thanks for contributing! You can leave the PR description blank and let [rigging](https://github.com/dreadnode/rigging) perform some magic here.
\ No newline at end of file
diff --git a/.github/scripts/rigging_pr_decorator.py b/.github/scripts/rigging_pr_decorator.py
new file mode 100644
index 0000000..fb41af0
--- /dev/null
+++ b/.github/scripts/rigging_pr_decorator.py
@@ -0,0 +1,145 @@
+import asyncio
+import base64
+import os
+import typing as t
+
+from pydantic import ConfigDict, StringConstraints
+
+import rigging as rg
+from rigging import logger
+from rigging.generator import GenerateParams, Generator, register_generator
+
+logger.enable("rigging")
+
+MAX_TOKENS = 8000
+TRUNCATION_WARNING = "\n\n**Note**: Due to the large size of this diff, some content has been truncated."
+str_strip = t.Annotated[str, StringConstraints(strip_whitespace=True)]
+
+
+class PRDiffData(rg.Model):
+ """XML model for PR diff data"""
+
+ content: str_strip = rg.element()
+
+ @classmethod
+ def xml_example(cls) -> str:
+ return """example diff content"""
+
+
+class PRDecorator(Generator):
+ """Generator for creating PR descriptions"""
+
+ model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
+
+ api_key: str = ""
+ max_tokens: int = MAX_TOKENS
+
+ def __init__(self, model: str, params: rg.GenerateParams) -> None:
+ api_key = params.extra.get("api_key")
+ if not api_key:
+ raise ValueError("api_key is required in params.extra")
+
+ super().__init__(model=model, params=params, api_key=api_key)
+ self.api_key = api_key
+ self.max_tokens = params.max_tokens or MAX_TOKENS
+
+ async def generate_messages(
+ self,
+ messages: t.Sequence[t.Sequence[rg.Message]],
+ params: t.Sequence[GenerateParams],
+ ) -> t.Sequence[rg.GeneratedMessage]:
+ responses = []
+ for message_seq, p in zip(messages, params):
+ base_generator = rg.get_generator(self.model, params=p)
+ llm_response = await base_generator.generate_messages([message_seq], [p])
+ responses.extend(llm_response)
+ return responses
+
+
+register_generator("pr_decorator", PRDecorator)
+
+
+async def generate_pr_description(diff_text: str) -> str:
+ """Generate a PR description from the diff text"""
+ diff_tokens = len(diff_text) // 4
+ if diff_tokens >= MAX_TOKENS:
+ char_limit = (MAX_TOKENS * 4) - len(TRUNCATION_WARNING)
+ diff_text = diff_text[:char_limit] + TRUNCATION_WARNING
+
+ diff_data = PRDiffData(content=diff_text)
+ params = rg.GenerateParams(
+ extra={
+ "api_key": os.environ["OPENAI_API_KEY"],
+ "diff_text": diff_text,
+ },
+ temperature=0.7,
+ max_tokens=500,
+ )
+
+ generator = rg.get_generator("pr_decorator!gpt-4-turbo-preview", params=params)
+ prompt = f"""You are a helpful AI that generates clear and concise PR descriptions.
+ Analyze the provided diff between {PRDiffData.xml_example()} tags and create a summary using exactly this format:
+
+ ### PR Summary
+
+ #### Overview of Changes
+
+
+ #### Key Modifications
+ 1. ****:
+ 2. ****:
+ 3. ****:
+ (continue as needed)
+
+ #### Potential Impact
+ -
+ -
+ -
+ (continue as needed)
+
+ Here is the PR diff to analyze:
+ {diff_data.to_xml()}"""
+
+ chat = await generator.chat(prompt).run()
+ return chat.last.content.strip()
+
+
+async def main():
+ """Main function for CI environment"""
+ if not os.environ.get("OPENAI_API_KEY"):
+ raise ValueError("OPENAI_API_KEY environment variable must be set")
+
+ try:
+ diff_text = os.environ.get("GIT_DIFF", "")
+ if not diff_text:
+ raise ValueError("No diff found in GIT_DIFF environment variable")
+
+ try:
+ diff_text = base64.b64decode(diff_text).decode("utf-8")
+ except Exception:
+ padding = 4 - (len(diff_text) % 4)
+ if padding != 4:
+ diff_text += "=" * padding
+ diff_text = base64.b64decode(diff_text).decode("utf-8")
+
+ logger.debug(f"Processing diff of length: {len(diff_text)}")
+ description = await generate_pr_description(diff_text)
+
+ with open(os.environ["GITHUB_OUTPUT"], "a") as f:
+ f.write("content<> $GITHUB_OUTPUT
+
+ - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b #v5.0.3
+ with:
+ python-version: "3.11"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip cache purge
+ pip install pydantic==2.9.1
+ pip install rigging[all]
+
+ # Generate the description using the diff
+ - name: Generate PR Description
+ id: description
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ GIT_DIFF: ${{ steps.diff.outputs.diff }}
+ run: |
+ python .github/scripts/rigging_pr_decorator.py
+
+ # Update the PR description
+ - name: Update PR Description
+ uses: nefrob/pr-description@4dcc9f3ad5ec06b2a197c5f8f93db5e69d2fdca7 #v1.2.0
+ with:
+ content: |
+ ## AI-Generated Summary
+
+ ${{ steps.description.outputs.content }}
+
+ ---
+
+ This summary was generated with ❤️ by [rigging](https://rigging.dreadnode.io/)
+ regex: ".*"
+ regexFlags: s
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/validate_robopages.yml b/.github/workflows/validate_robopages.yml
index b379e82..fc70294 100644
--- a/.github/workflows/validate_robopages.yml
+++ b/.github/workflows/validate_robopages.yml
@@ -27,10 +27,11 @@ jobs:
- name: Validate Contribution Files
id: robopages-validation
- continue-on-error: true
run: |
validate_file() {
local file="$1"
+ local tmp_file="/tmp/$(basename $file)"
+ local validation_status=0
if [[ ! "$file" =~ ^([a-zA-Z0-9_\-]+/)*[a-zA-Z0-9_\-]+\.yml$ ]]; then
echo "Invalid file path characters: $file"
@@ -42,17 +43,33 @@ jobs:
return 1
fi
+ # Create copy and inject categories if missing
+ cp "$file" "$tmp_file"
+ if ! grep -q "categories:" "$tmp_file"; then
+ # Extract categories from path
+ categories=$(dirname "$file" | tr '/' '\n' | awk 'NF' | sed 's/^/ - /')
+ # Inject categories into YAML
+ echo -e "\ncategories:\n$categories" >> "$tmp_file"
+ fi
+
docker pull dreadnode/robopages:latest
- # Run validation with Docker socket mounted
+ # Run validation and capture the exit status
docker run --rm \
-v $(pwd):/workspace \
-v /var/run/docker.sock:/var/run/docker.sock \
+ -v "$tmp_file:/workspace/$(basename $file)" \
-w /workspace \
--privileged \
- dreadnode/robopages:latest validate --path "$(printf '%q' "$file")" --skip-docker
+ dreadnode/robopages:latest validate --path "$(basename $file)" --skip-docker || validation_status=$?
+
+ rm "$tmp_file"
+ return $validation_status
}
+ # Initialize overall status
+ overall_status=0
+
# Get changed files using GitHub's provided variables
changed_files=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | \
grep '\.yml$' | grep -v '^.github/' || true)
@@ -60,15 +77,22 @@ jobs:
# Validate each changed file
for file in $changed_files; do
echo "Validating $file..."
- validate_file "$file" || exit 1
+ if ! validate_file "$file"; then
+ overall_status=1
+ echo "::error::Validation failed for $file"
+ fi
done
+ exit $overall_status
+
- name: Post validation status
if: always()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #7.0.1
with:
script: |
- const validation_status = '${{ steps.robopages-validation.outcome }}' === 'success' ? '✅ Validation successful' : '❌ Validation failed';
+ const validation_status = process.env.STATE_validation === '0'
+ ? '✅ Validation successful'
+ : '❌ Validation failed';
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const timestamp = new Date().toISOString();
const body = [
@@ -78,17 +102,16 @@ jobs:
'',
'Please ensure your contribution follows the required format.',
'',
- `🔍 [View Full Validation Details](${runUrl})`,
+ `[View Full Validation Details](${runUrl})`,
'',
'---',
`Run ID: \`${process.env.GITHUB_RUN_ID}\``,
`Workflow: ${process.env.GITHUB_WORKFLOW}`
].join('\n');
- github.rest.pulls.createReview({
+ github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
- pull_number: context.issue.number,
- body: body,
- event: 'COMMENT'
+ issue_number: context.issue.number,
+ body: body
});
\ No newline at end of file
diff --git a/cybersecurity/cicd/squealer.Dockerfile b/cybersecurity/cicd/squealer.Dockerfile
new file mode 100644
index 0000000..c29f378
--- /dev/null
+++ b/cybersecurity/cicd/squealer.Dockerfile
@@ -0,0 +1,21 @@
+# Git clone stage
+FROM alpine:latest AS source
+RUN apk add --no-cache git
+WORKDIR /src
+RUN git clone https://github.com/owenrumney/squealer.git . && \
+ ls -la # Debug: verify files
+
+# Build stage
+FROM golang:1.21-alpine AS builder
+WORKDIR /build
+COPY --from=source /src/ ./
+RUN ls -la && \
+ go mod vendor && \
+ go build -mod=vendor -ldflags="-w -s" -o squealer ./cmd/squealer
+
+# Final stage
+FROM gcr.io/distroless/static-debian12:nonroot
+WORKDIR /app
+COPY --from=builder /build/squealer /app/
+USER nonroot:nonroot
+ENTRYPOINT ["/app/squealer"]
\ No newline at end of file
diff --git a/cybersecurity/cicd/squealer.yml b/cybersecurity/cicd/squealer.yml
new file mode 100644
index 0000000..61247ea
--- /dev/null
+++ b/cybersecurity/cicd/squealer.yml
@@ -0,0 +1,37 @@
+description: Squealer is a tool that finds secrets like keys, tokens and passwords in your code. It scans remote Git repositories for potential credential leaks.
+
+categories:
+ - cybersecurity
+ - cicd
+
+functions:
+ squealer_scan_git_repo:
+ description: Scan a remote git repository for secrets and credentials
+ parameters:
+ repository:
+ type: string
+ description: Git repository URL (SSH format)
+ examples:
+ - "git@github.com:username/repo.git"
+ - "git@github.com:owenrumney/woopsie.git"
+ container:
+ force: true
+ image: squealer:latest
+ cmdline:
+ - ${repository}
+
+ squealer_scan_everything_git_repo:
+ description: Scan a remote git repository and history for secrets and credentials with everything flag
+ parameters:
+ repository:
+ type: string
+ description: Git repository URL (SSH format)
+ examples:
+ - "git@github.com:username/repo.git"
+ - "git@github.com:owenrumney/woopsie.git"
+ container:
+ force: true
+ image: squealer:latest
+ cmdline:
+ - ${repository}
+ - --everything
diff --git a/cybersecurity/offensive/web-exploitation/zscan.Dockerfile b/cybersecurity/offensive/web-exploitation/zscan.Dockerfile
new file mode 100644
index 0000000..c276ece
--- /dev/null
+++ b/cybersecurity/offensive/web-exploitation/zscan.Dockerfile
@@ -0,0 +1,35 @@
+# Git clone stage
+FROM alpine:latest AS source
+RUN apk add --no-cache git
+WORKDIR /src
+RUN git clone https://github.com/zcyberseclab/zscan.git . || exit 1
+
+# Build stage - update Go version
+FROM golang:1.23.2-alpine AS builder
+WORKDIR /build
+COPY --from=source /src .
+
+# Set Go build flags
+ENV CGO_ENABLED=0 \
+ GOOS=linux \
+ GOARCH=amd64 \
+ GO111MODULE=on
+
+# Build optimized binary
+RUN go mod download && \
+ go build -ldflags="-w -s" -o zscan cmd/main.go
+
+# Final stage
+FROM gcr.io/distroless/static-debian12:nonroot
+WORKDIR /app
+
+# Copy only necessary artifacts
+COPY --from=builder /build/zscan /app/
+COPY --from=builder /build/config /app/config
+COPY --from=builder /build/templates /app/templates
+
+# Container configuration
+USER nonroot:nonroot
+EXPOSE 8080
+
+ENTRYPOINT ["/app/zscan"]
\ No newline at end of file
diff --git a/cybersecurity/offensive/web-exploitation/zscan.yml b/cybersecurity/offensive/web-exploitation/zscan.yml
new file mode 100644
index 0000000..8f6100c
--- /dev/null
+++ b/cybersecurity/offensive/web-exploitation/zscan.yml
@@ -0,0 +1,62 @@
+description: >
+ Zscan is a security scanning tool built in Go that provides network exploration
+ and vulnerability assessment capabilities. It combines multiple security tools
+ and techniques into a single interface for comprehensive security testing.
+
+categories:
+ - cybersecurity
+ - offensive
+ - web-expliotation
+
+functions:
+ zscan_default_scan:
+ description: Perform a default security scan against specified targets
+ parameters:
+ target:
+ type: string
+ description: The target IP address or CIDR range to scan
+ examples:
+ - 192.168.1.1
+ - 10.0.0.0/24
+ - 127.0.0.1
+
+ container:
+ build:
+ path: ${cwd}/zscan.Dockerfile
+ name: zscan_local
+ args:
+ - --net=host
+ volumes:
+ - ${cwd}:/data
+
+ cmdline:
+ - /app/zscan
+ - -target
+ - ${target}
+
+ zscan_full_scan:
+ description: Perform a comprehensive security scan
+ parameters:
+ target:
+ type: string
+ description: The target IP address or CIDR range to scan
+ threads:
+ type: integer
+ description: Number of concurrent scanning threads
+ default: 10
+
+ container:
+ build:
+ path: ${cwd}/zscan.Dockerfile
+ name: zscan_local
+ args:
+ - --net=host
+ volumes:
+ - ${cwd}:/data
+
+ cmdline:
+ - /app/zscan
+ - -target
+ - ${target}
+ - -threads
+ - ${threads}