Skip to content

Commit 1017157

Browse files
authored
Add IBM i Security Operations Tools and Agent (#21)
* feat: add security vulnerability assessment and remediation tools Add comprehensive YAML-based security tools for IBM i systems including: - User management tools (limited capabilities, command permissions) - Vulnerability assessment tools (file permissions, user profiles, attack vectors) - Security audit tools (special authority tracking, command audit settings) - Remediation tools (impersonation lockdown generation and execution) Organized into three toolsets: security_vulnerability_assessment, security_audit, and security_remediation Signed-off-by: itsdevansh <[email protected]> * feat: Add Security Operations Agent with category filtering Signed-off-by: itsdevansh <[email protected]> * feat: implement dynamic human-in-the-loop middleware for security tools - Add _get_non_readonly_tools() helper to extract tools with readOnly: false - Modify create_security_ops_agent() to dynamically build HumanInTheLoopMiddleware - Automatically apply approval workflow to tools marked as non-readonly in YAML - Add enable_human_in_loop parameter to allow disabling middleware Signed-off-by: itsdevansh <[email protected]> --------- Signed-off-by: itsdevansh <[email protected]>
1 parent 648b9a7 commit 1017157

File tree

4 files changed

+669
-17
lines changed

4 files changed

+669
-17
lines changed

agents/frameworks/langchain/src/ibmi_agents/agents/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
Available agents:
88
- Performance Agent: System performance monitoring and analysis
99
- SysAdmin Discovery Agent: High-level system discovery and summarization
10-
- SysAdmin Browse Agent: Detailed system browsing and exploration
10+
- SysAdmin Browse Agent: Detailed system browsing and exploration
1111
- SysAdmin Search Agent: System search and lookup capabilities
12+
- Security Operations Agent: Security vulnerability assessment and remediation
1213
"""
1314

1415
from .ibmi_agents import (
@@ -17,6 +18,7 @@
1718
create_sysadmin_discovery_agent,
1819
create_sysadmin_browse_agent,
1920
create_sysadmin_search_agent,
21+
create_security_ops_agent,
2022
chat_with_agent,
2123
list_available_agents,
2224
set_verbose_logging,
@@ -30,6 +32,7 @@
3032
"create_sysadmin_discovery_agent",
3133
"create_sysadmin_browse_agent",
3234
"create_sysadmin_search_agent",
35+
"create_security_ops_agent",
3336
"chat_with_agent",
3437
"list_available_agents",
3538
"set_verbose_logging",

agents/frameworks/langchain/src/ibmi_agents/agents/ibmi_agents.py

Lines changed: 148 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
import os
1919
import json
2020
import getpass
21-
from typing import Dict, Any, List
21+
from typing import Dict, Any, List, Optional
2222
from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
2323
from langchain_ollama import ChatOllama
2424
from langchain_openai import ChatOpenAI
2525
from langchain_anthropic import ChatAnthropic
2626
from langchain.agents import create_agent
27+
from langchain.agents.middleware import HumanInTheLoopMiddleware
2728
from langchain_mcp_adapters.client import MultiServerMCPClient
2829

2930
# Import MCP tools from the SDK package
@@ -133,6 +134,38 @@ def get_model(model_id: str = "gpt-oss:20b", temperature: float = 0.3):
133134
else:
134135
return ChatOllama(model=model_id, temperature=temperature)
135136

137+
# -----------------------------------------------------------------------------
138+
# Tool Metadata Helper Functions
139+
# -----------------------------------------------------------------------------
140+
141+
def _get_non_readonly_tools(tools: List[Any]) -> List[str]:
142+
"""
143+
Extract tool names that have readOnly: false in their metadata.
144+
145+
This function checks the tool metadata for the 'readOnlyHint' annotation
146+
(which corresponds to the security.readOnly field in YAML) and returns
147+
a list of tool names that are marked as non-readonly (write operations).
148+
149+
Args:
150+
tools: List of LangChain tool objects with metadata
151+
152+
Returns:
153+
List of tool names that require human approval (readOnly: false)
154+
"""
155+
non_readonly_tools = []
156+
157+
for tool in tools:
158+
# Check if tool has metadata
159+
if hasattr(tool, 'metadata') and tool.metadata:
160+
# Check readOnlyHint annotation (corresponds to security.readOnly in YAML)
161+
read_only_hint = tool.metadata.get('readOnlyHint', True)
162+
163+
# If readOnlyHint is False, this is a write operation that needs approval
164+
if read_only_hint is False:
165+
non_readonly_tools.append(tool.name)
166+
167+
return non_readonly_tools
168+
136169
# -----------------------------------------------------------------------------
137170
# Agent Creation Functions
138171
# -----------------------------------------------------------------------------
@@ -370,6 +403,116 @@ async def agent_session():
370403

371404
return agent_session()
372405

406+
async def create_security_ops_agent(
407+
model_id: str = "gpt-oss:20b",
408+
mcp_url: str = DEFAULT_MCP_URL,
409+
transport: str = DEFAULT_TRANSPORT,
410+
category: Optional[str] = None,
411+
enable_human_in_loop: bool = True,
412+
**kwargs
413+
):
414+
"""
415+
Create IBM i Security Operations Agent.
416+
417+
Args:
418+
model_id: Model identifier (default: "gpt-oss:20b")
419+
mcp_url: MCP server URL
420+
transport: Transport type
421+
category: Optional category filter for security tools. Options:
422+
- "vulnerability-assessment": Tools for identifying security vulnerabilities
423+
- "audit": Tools for auditing security configurations
424+
- "remediation": Tools for generating and executing security fixes
425+
- "user-management": Tools for managing user capabilities and permissions
426+
- None: Load all security tools (default)
427+
enable_human_in_loop: Enable human-in-the-loop middleware for non-readonly tools (default: True)
428+
**kwargs: Additional agent configuration options
429+
430+
Returns an async context manager that yields (agent, session).
431+
Usage: async with (await create_security_ops_agent()) as (agent, session): ...
432+
"""
433+
client = get_mcp_client(mcp_url, transport)
434+
435+
@asynccontextmanager
436+
async def agent_session():
437+
async with client.session("ibmi_tools") as session:
438+
# Load security tools with optional category filtering
439+
if category:
440+
# Use annotation filtering to load tools by domain and category
441+
tools = await load_filtered_mcp_tools(
442+
session,
443+
annotation_filters={
444+
"domain": "security",
445+
"category": category
446+
},
447+
debug=True
448+
)
449+
print(f"✅ Loaded {len(tools)} security operations tools (category: {category}) for Security Ops Agent")
450+
else:
451+
# Load all security tools by domain
452+
tools = await load_filtered_mcp_tools(
453+
session,
454+
annotation_filters={"domain": "security"},
455+
debug=True
456+
)
457+
print(f"✅ Loaded {len(tools)} security operations tools for Security Ops Agent")
458+
459+
# Build human-in-the-loop middleware dynamically based on tool annotations
460+
middleware = []
461+
if enable_human_in_loop:
462+
non_readonly_tools = _get_non_readonly_tools(tools)
463+
if non_readonly_tools:
464+
interrupt_config = {}
465+
for tool_name in non_readonly_tools:
466+
interrupt_config[tool_name] = {
467+
"allowed_decision": ["approve", "reject"],
468+
}
469+
470+
middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_config))
471+
print(f"🔒 Human-in-the-loop enabled for {len(non_readonly_tools)} non-readonly tools:")
472+
for tool_name in non_readonly_tools:
473+
print(f" - {tool_name}")
474+
475+
system_message = """You are a specialized IBM i security operations assistant.
476+
You help administrators identify security vulnerabilities, audit system configurations, and remediate security issues.
477+
Your role is to:
478+
- Identify security vulnerabilities and misconfigurations
479+
- Assess user privileges and special authorities
480+
- Audit file and object permissions for *PUBLIC access
481+
- Detect potential attack vectors (triggers, impersonation, privilege escalation)
482+
- Generate remediation commands for security lockdown
483+
- Explain security risks in business terms
484+
- Provide actionable recommendations for hardening system security
485+
486+
IMPORTANT SECURITY NOTES:
487+
- Always explain the security implications of findings
488+
- Distinguish between read-only assessment tools and destructive remediation tools
489+
- For remediation tools, you will be prompted for approval before execution
490+
- Recommend testing remediation commands in development before production
491+
- Prioritize findings by severity (critical vulnerabilities first)
492+
493+
Focus on helping administrators understand their security posture and take appropriate action to protect their IBM i systems."""
494+
495+
llm = get_model(model_id)
496+
497+
# Only pass middleware if it's not empty
498+
agent_kwargs = {
499+
"model": llm,
500+
"tools": tools,
501+
"system_prompt": system_message,
502+
"checkpointer": get_shared_checkpointer(),
503+
"store": get_shared_store(),
504+
"name": "IBM i Security Operations",
505+
**kwargs
506+
}
507+
508+
if middleware:
509+
agent_kwargs["middleware"] = middleware
510+
511+
agent = create_agent(**agent_kwargs)
512+
yield agent, session
513+
514+
return agent_session()
515+
373516
# -----------------------------------------------------------------------------
374517
# Agent Registry and Factory Pattern
375518
# -----------------------------------------------------------------------------
@@ -379,6 +522,7 @@ async def agent_session():
379522
"discovery": create_sysadmin_discovery_agent,
380523
"browse": create_sysadmin_browse_agent,
381524
"search": create_sysadmin_search_agent,
525+
"security": create_security_ops_agent,
382526
}
383527

384528
async def create_ibmi_agent(agent_type: str, **kwargs) -> _AsyncGeneratorContextManager[tuple[Any, Any], None]:
@@ -404,7 +548,8 @@ def list_available_agents() -> Dict[str, str]:
404548
"performance": "System performance monitoring and analysis",
405549
"discovery": "High-level system discovery and summarization",
406550
"browse": "Detailed system browsing and exploration",
407-
"search": "System search and lookup capabilities"
551+
"search": "System search and lookup capabilities",
552+
"security": "Security vulnerability assessment and remediation"
408553
}
409554

410555
def set_verbose_logging(enabled: bool):
@@ -609,7 +754,7 @@ async def test_agents():
609754
"gpt-oss:20b", # Default Ollama model
610755
"ollama:llama3.1", # Explicit Ollama model
611756
"openai:gpt-4o", # OpenAI model
612-
"anthropic:claude-3.7-sonnet" # Anthropic model
757+
"aanthropic:claude-3-7-sonnet-20250219" # Anthropic model
613758
]
614759

615760
print("Available model options:")

agents/frameworks/langchain/src/ibmi_agents/agents/test_agents.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import asyncio
1313
import sys
1414
import os
15+
from typing import Optional
1516

1617
# Disable LangSmith tracing for tests
1718
os.environ["LANGCHAIN_TRACING_V2"] = "false"
@@ -30,19 +31,30 @@
3031
"performance": "What is my system status? Give me CPU and memory metrics.",
3132
"discovery": "Give me an overview of available system services.",
3233
"browse": "Show me services in the QSYS2 schema.",
33-
"search": "Search for services related to system status."
34+
"search": "Search for services related to system status.",
35+
"security": "Check for user profiles vulnerable to impersonation attacks."
3436
}
3537

36-
async def test_single_agent(agent_type: str, model_id: str = "gpt-oss:20b"):
38+
async def test_single_agent(agent_type: str, model_id: str = "gpt-oss:20b", category: Optional[str] = None):
3739
"""Test a single agent with a sample query."""
3840
print(f"\n{'='*80}")
3941
print(f"Testing {agent_type.upper()} Agent")
42+
if category:
43+
print(f"Category Filter: {category}")
4044
print(f"{'='*80}\n")
4145

4246
try:
4347
# Create agent context
4448
print(f"🔧 Creating {agent_type} agent with model {model_id}...")
45-
ctx = await create_ibmi_agent(agent_type, model_id=model_id)
49+
if category:
50+
print(f" Filtering by category: {category}")
51+
52+
# Pass category parameter if provided (only for security agent)
53+
kwargs = {"model_id": model_id}
54+
if category and agent_type == "security":
55+
kwargs["category"] = category
56+
57+
ctx = await create_ibmi_agent(agent_type, **kwargs)
4658

4759
async with ctx as (agent, session):
4860
print(f"✅ Agent created: {agent.name}\n")
@@ -111,16 +123,27 @@ async def test_all_agents(model_id: str = "gpt-oss:20b"):
111123

112124
return all(results.values())
113125

114-
async def interactive_mode(agent_type: str, model_id: str = "gpt-oss:20b"):
126+
async def interactive_mode(agent_type: str, model_id: str = "gpt-oss:20b", category: Optional[str] = None):
115127
"""Interactive chat mode with a specific agent."""
116128
print(f"\n{'='*80}")
117129
print(f"Interactive Mode - {agent_type.upper()} Agent")
130+
if category:
131+
print(f"Category Filter: {category}")
118132
print(f"{'='*80}\n")
119133

120134
try:
121135
# Create agent context
122-
print(f"🔧 Initializing {agent_type} agent with {model_id}...\n")
123-
ctx = await create_ibmi_agent(agent_type, model_id=model_id)
136+
print(f"🔧 Initializing {agent_type} agent with {model_id}...")
137+
if category:
138+
print(f" Filtering by category: {category}")
139+
print()
140+
141+
# Pass category parameter if provided (only for security agent)
142+
kwargs = {"model_id": model_id}
143+
if category and agent_type == "security":
144+
kwargs["category"] = category
145+
146+
ctx = await create_ibmi_agent(agent_type, **kwargs)
124147

125148
async with ctx as (agent, session):
126149
print(f"✅ {agent.name} ready!\n")
@@ -171,19 +194,32 @@ async def interactive_mode(agent_type: str, model_id: str = "gpt-oss:20b"):
171194
import traceback
172195
traceback.print_exc()
173196

174-
async def quick_test(model_id: str = "gpt-oss:20b"):
197+
async def quick_test(model_id: str = "gpt-oss:20b", category: Optional[str] = None, agent_filter: Optional[str] = None):
175198
"""Quick test - just verify all agents can be created."""
176199
print("\n" + "="*80)
177200
print("Quick Agent Creation Test")
178201
print("="*80)
179-
print(f"Model: {model_id}\n")
202+
print(f"Model: {model_id}")
203+
if category:
204+
print(f"Category Filter: {category}")
205+
print()
180206

181207
results = {}
182208

183-
for agent_type in AVAILABLE_AGENTS.keys():
209+
# Filter to specific agent if requested
210+
agents_to_test = [agent_filter] if agent_filter else list(AVAILABLE_AGENTS.keys())
211+
212+
for agent_type in agents_to_test:
184213
try:
185214
print(f"Creating {agent_type} agent...", end=" ")
186-
ctx = await create_ibmi_agent(agent_type, model_id=model_id)
215+
216+
# Pass category parameter if provided (only for security agent)
217+
kwargs = {"model_id": model_id}
218+
if category and agent_type == "security":
219+
kwargs["category"] = category
220+
print(f"(category: {category})...", end=" ")
221+
222+
ctx = await create_ibmi_agent(agent_type, **kwargs)
187223
async with ctx as (agent, session):
188224
print(f"✅ {agent.name}")
189225
results[agent_type] = True
@@ -235,6 +271,12 @@ def main():
235271
help="Test specific agent type"
236272
)
237273

274+
parser.add_argument(
275+
"--category",
276+
choices=["vulnerability-assessment", "audit", "remediation", "user-management"],
277+
help="Filter security agent tools by category (only applies to security agent)"
278+
)
279+
238280
parser.add_argument(
239281
"--interactive",
240282
action="store_true",
@@ -278,15 +320,21 @@ def main():
278320
if args.interactive and not args.agent:
279321
parser.error("--interactive requires --agent to be specified")
280322

323+
if args.category and not args.agent:
324+
parser.error("--category requires --agent to be specified")
325+
326+
if args.category and args.agent != "security":
327+
parser.error("--category can only be used with --agent security")
328+
281329
# Run appropriate test mode
282330
try:
283331
if args.quick:
284-
success = asyncio.run(quick_test(args.model))
332+
success = asyncio.run(quick_test(args.model, args.category, args.agent))
285333
elif args.interactive:
286-
asyncio.run(interactive_mode(args.agent, args.model))
334+
asyncio.run(interactive_mode(args.agent, args.model, args.category))
287335
success = True
288336
elif args.agent:
289-
success = asyncio.run(test_single_agent(args.agent, args.model))
337+
success = asyncio.run(test_single_agent(args.agent, args.model, args.category))
290338
else:
291339
success = asyncio.run(test_all_agents(args.model))
292340

0 commit comments

Comments
 (0)