drydock-cli 3.0.171__tar.gz → 3.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {drydock_cli-3.0.171/drydock_cli.egg-info → drydock_cli-3.1.2}/PKG-INFO +1 -1
- drydock_cli-3.1.2/drydock/builtin_skills/fiar-assess.md +33 -0
- drydock_cli-3.1.2/drydock/builtin_skills/fiar-cap.md +26 -0
- drydock_cli-3.1.2/drydock/builtin_skills/fiar-evidence.md +33 -0
- drydock_cli-3.1.2/drydock/builtin_skills/fiar-readiness.md +28 -0
- drydock_cli-3.1.2/drydock/fiar.py +528 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/loop_detect.py +91 -3
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/providers.py +101 -1
- drydock_cli-3.1.2/drydock/suggest.py +125 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tools/__init__.py +282 -7
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/app.py +41 -10
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/widgets.py +7 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/SOURCES.txt +6 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/pyproject.toml +1 -1
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_degenerate_argument.py +20 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_dispatch.py +26 -0
- drydock_cli-3.1.2/tests/test_fiar.py +211 -0
- drydock_cli-3.1.2/tests/test_loop_detect.py +194 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_mcp.py +5 -2
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_skills.py +5 -1
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_stig.py +9 -0
- drydock_cli-3.1.2/tests/test_suggest.py +73 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_arg_coercion.py +21 -1
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_arg_coercion_more.py +11 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_arg_parsing.py +35 -0
- drydock_cli-3.0.171/drydock/suggest.py +0 -30
- drydock_cli-3.0.171/tests/test_loop_detect.py +0 -101
- drydock_cli-3.0.171/tests/test_suggest.py +0 -30
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/LICENSE +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/NOTICE +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/README.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/agent.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/budget.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/cci.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/cli.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/config.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/detect.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/events.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/extract.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/guards.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/phases.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/poam.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/progress.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/recovery.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/resume.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/skills.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/stig.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/subagents.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_policy.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_result.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_select.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tool_validate.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/trajectory.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/verification.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock/web.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/setup.cfg +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_budget.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_config.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_context_limit_issue25.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_criteria_coverage.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_cycling_detection.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_effort_governor.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_events.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_graphrag_sqlite.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_model_registry.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_phases.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_progress.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recovery.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recovery_config.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_recovery_integration.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_repeated_outcome.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_resume_events.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_resume_restore.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_resume_snapshot.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_sqlite_events.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_subagents.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_timeline.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_canonical.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_policy.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_select.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tool_validate.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_trajectory.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_worker_subagent.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.171 → drydock_cli-3.1.2}/tests/test_xccdf.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fiar-assess
|
|
3
|
+
description: Test the next un-tested FIAR control objective against real evidence
|
|
4
|
+
---
|
|
5
|
+
Test FIAR control objectives in the engagement at: $ARGS
|
|
6
|
+
|
|
7
|
+
Do exactly ONE control per run (loop it — /loop N /fiar-assess <path> — for the
|
|
8
|
+
whole matrix):
|
|
9
|
+
|
|
10
|
+
1. Call FiarControls with that path and status=not_tested. If none remain, say the
|
|
11
|
+
control matrix is fully tested and STOP without calling other tools.
|
|
12
|
+
2. Take the FIRST not_tested control. Call FiarControl (path + id) to read its
|
|
13
|
+
control objective, assertions, required Key Supporting Documents (KSDs), and
|
|
14
|
+
test procedure.
|
|
15
|
+
3. You MUST examine real evidence before deciding — do NOT guess. Follow the test
|
|
16
|
+
procedure: Read the actual KSD the control names (obligating document, receiving
|
|
17
|
+
report, invoice, reconciliation workpaper, SF-50, access-recert listing, …) or
|
|
18
|
+
run its check command (Bash). If the procedure says to sample, state your sample
|
|
19
|
+
size and how many exceptions you found. Reason step by step about whether the
|
|
20
|
+
evidence satisfies the control objective for its assertion(s).
|
|
21
|
+
4. Call FiarAssess (path + id) with:
|
|
22
|
+
- status = effective ONLY if the evidence clearly shows the control operated;
|
|
23
|
+
deficient if the evidence shows it did not (any un-remediated
|
|
24
|
+
exception makes the control deficient);
|
|
25
|
+
not_applicable if the control does not apply to this entity;
|
|
26
|
+
- evidence = the exact KSDs you examined and what they showed (quote the line);
|
|
27
|
+
- sample_size and exceptions when you tested a sample.
|
|
28
|
+
5. If you set deficient, immediately call FiarFinding to record the NFR: condition
|
|
29
|
+
(what you observed), criteria (the policy/standard/GAAP requirement), cause
|
|
30
|
+
(root cause), effect (impact on the assertion/balance), recommendation, and a
|
|
31
|
+
severity (deficiency / significant_deficiency / material_weakness).
|
|
32
|
+
|
|
33
|
+
Give a one-line summary: control id, the KSD evidence, and the status you set.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fiar-cap
|
|
3
|
+
description: Draft a Corrective Action Plan for the open FIAR findings (NFRs)
|
|
4
|
+
---
|
|
5
|
+
Draft Corrective Action Plans for the open findings in the FIAR engagement at: $ARGS
|
|
6
|
+
|
|
7
|
+
1. Call FiarControls (path only) to see the engagement, then read the findings by
|
|
8
|
+
listing deficient controls (FiarControls status=deficient) and, for each, its
|
|
9
|
+
recorded NFR. If there are no findings, say so and STOP.
|
|
10
|
+
2. Take ONE finding at a time (worst severity first: material_weakness, then
|
|
11
|
+
significant_deficiency, then deficiency). For it, draft a CAP that is specific
|
|
12
|
+
and testable:
|
|
13
|
+
- root_cause: the underlying reason the control failed (people / process /
|
|
14
|
+
system / policy) — go past the symptom in the NFR condition;
|
|
15
|
+
- action: the concrete corrective action that will make the control operate;
|
|
16
|
+
- responsible: the organization / POC who owns it;
|
|
17
|
+
- target_date: a realistic milestone date;
|
|
18
|
+
- milestones: 2–4 dated, verifiable steps (e.g. "update SOP", "train preparers",
|
|
19
|
+
"re-test 25 samples with 0 exceptions").
|
|
20
|
+
3. The CAP must close the specific assertion gap the NFR identified — tie the final
|
|
21
|
+
milestone to a re-test of the same control with zero exceptions. Note whether the
|
|
22
|
+
fix is a process change, a system change, or an ITGC (access / SoD / change
|
|
23
|
+
management) remediation, since those route to different owners.
|
|
24
|
+
|
|
25
|
+
Present each CAP in the standard structure (finding → root cause → action →
|
|
26
|
+
responsible → milestones), and end with which finding to remediate first and why.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fiar-evidence
|
|
3
|
+
description: Trace the full FIAR evidence chain for a control before passing it
|
|
4
|
+
---
|
|
5
|
+
Substantively test ONE FIAR control by tracing the complete evidence chain, for the
|
|
6
|
+
engagement + control id in: $ARGS
|
|
7
|
+
|
|
8
|
+
A control cannot support a financial-statement assertion on a broken audit trail.
|
|
9
|
+
You MUST trace all eight links before FiarAssess will let you mark it effective:
|
|
10
|
+
|
|
11
|
+
population → sample → source_transaction → authorization → supporting_document
|
|
12
|
+
→ system_posting → gl_effect → assertion
|
|
13
|
+
|
|
14
|
+
1. Call FiarControl (path + id) to read the objective, assertions and KSDs.
|
|
15
|
+
2. Establish the POPULATION: the recorded universe (a GL detail, a transaction
|
|
16
|
+
register, an APSR listing). State how you know it is complete.
|
|
17
|
+
3. SELECT a sample from that population and record exactly which item(s).
|
|
18
|
+
4. For each sampled item, gather and Read the real evidence for each link:
|
|
19
|
+
- source_transaction: the transaction the item represents;
|
|
20
|
+
- authorization: the warranted/authorized approver (warrant, SF-50, delegation);
|
|
21
|
+
- supporting_document: the KSD (contract, receiving report, invoice, recon wp);
|
|
22
|
+
- system_posting: where it posted in the feeder/accounting system;
|
|
23
|
+
- gl_effect: the resulting general-ledger entry;
|
|
24
|
+
- assertion: the FS assertion it ultimately supports.
|
|
25
|
+
5. Call FiarAssess with status=effective and a `chain` object whose keys are the
|
|
26
|
+
eight links above, each set to the specific evidence you found. If any link is
|
|
27
|
+
missing, FiarAssess will REFUSE and name the gap — go get that evidence, do not
|
|
28
|
+
force the status. If a link genuinely cannot be traced (e.g. no authorization on
|
|
29
|
+
file), the control is DEFICIENT: set status=deficient and record an NFR with
|
|
30
|
+
FiarFinding.
|
|
31
|
+
|
|
32
|
+
Report the item you traced and, link by link, the evidence that supported it (or the
|
|
33
|
+
link that broke the chain).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fiar-readiness
|
|
3
|
+
description: Assess audit-readiness posture of a FIAR engagement and report gaps
|
|
4
|
+
---
|
|
5
|
+
Produce a Discover-phase audit-readiness assessment for the FIAR engagement at: $ARGS
|
|
6
|
+
|
|
7
|
+
1. Call FiarControls (path only) to see the whole matrix: cycle, Wave, phase, and
|
|
8
|
+
the status counts. If the engagement JSON does not exist yet, tell the operator
|
|
9
|
+
to create one first: `python -m drydock.fiar new <path> <name> <cycle>` (cycles:
|
|
10
|
+
FBWT, P2P, PPE, INV, CIVPAY, REIM, FR, ITGC, or * for all).
|
|
11
|
+
2. Call FiarControls with status=deficient and status=not_tested to enumerate the
|
|
12
|
+
gaps. For each DEFICIENT control, note the assertion(s) at risk and whether an
|
|
13
|
+
NFR already exists for it.
|
|
14
|
+
3. Read the current readiness roll-up (the counts show tested vs effective). State,
|
|
15
|
+
in plain terms:
|
|
16
|
+
- percent of applicable controls tested, and of those, percent effective;
|
|
17
|
+
- the assertions (Existence, Completeness, Valuation, Presentation & Disclosure,
|
|
18
|
+
Rights & Obligations) that are NOT yet supported because a control covering
|
|
19
|
+
them is deficient or untested;
|
|
20
|
+
- open findings by severity (material weakness / significant deficiency /
|
|
21
|
+
deficiency) and whether each has a Corrective Action Plan.
|
|
22
|
+
4. Recommend the highest-value next actions to move toward assertion, prioritizing
|
|
23
|
+
material weaknesses and untested key controls (FBWT reconciliation, three-way
|
|
24
|
+
match, access/SoD ITGCs). Do NOT claim the entity is audit-ready — readiness is
|
|
25
|
+
asserted by management and validated by an independent examination; your job is
|
|
26
|
+
to report the evidenced posture and the gaps.
|
|
27
|
+
|
|
28
|
+
Report a short readiness summary (3–4 sentences) first, then the detail beneath it.
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""FIAR — Financial Improvement and Audit Readiness (DoD/War Dept audit readiness).
|
|
2
|
+
|
|
3
|
+
A stdlib-only engine for driving an audit-readiness engagement the way the FIAR
|
|
4
|
+
Guidance describes it: assess control objectives within a business-process cycle
|
|
5
|
+
against Key Supporting Documents (KSDs), record Notices of Findings and
|
|
6
|
+
Recommendations (NFRs), and track Corrective Action Plans (CAPs) through the
|
|
7
|
+
five FIAR methodology phases (Discover -> Correct -> Assert -> Validate ->
|
|
8
|
+
Sustain).
|
|
9
|
+
|
|
10
|
+
An "engagement" is one JSON file (analogous to a STIG .ckl): an assessable unit,
|
|
11
|
+
a control matrix seeded from the FIAR key-control-objective library, its findings,
|
|
12
|
+
and its CAPs. The agent reads and mutates it through the Fiar* tools; a human (or
|
|
13
|
+
eMASS/audit workpaper) can consume the JSON directly.
|
|
14
|
+
|
|
15
|
+
Reference: DoD FIAR Guidance (comptroller.war.gov). All code original to Drydock;
|
|
16
|
+
the control library encodes standard, publicly-documented FIAR control objectives.
|
|
17
|
+
Not a substitute for an IPA opinion — it structures the readiness work, it does
|
|
18
|
+
not render an audit judgement.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from dataclasses import dataclass, field, asdict
|
|
24
|
+
from datetime import date
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# ── Controlled vocabularies ───────────────────────────────────────────────
|
|
28
|
+
# The five commonly-accepted financial statement assertions the FIAR Guidance
|
|
29
|
+
# (April 2017, §1.B / §3) tests against — kept distinct, as the guidance lists
|
|
30
|
+
# them (existence and completeness are frequently TESTED together but are
|
|
31
|
+
# separate assertions).
|
|
32
|
+
ASSERTIONS = {
|
|
33
|
+
"E": "Existence",
|
|
34
|
+
"C": "Completeness",
|
|
35
|
+
"V": "Valuation & Accuracy",
|
|
36
|
+
"PD": "Presentation & Disclosure",
|
|
37
|
+
"RO": "Rights & Obligations",
|
|
38
|
+
}
|
|
39
|
+
# The FIAR Strategy prioritizes work into Four Waves toward a full-statement
|
|
40
|
+
# audit (Guidance Figure 1-1 / §2.C, §5).
|
|
41
|
+
WAVES = {
|
|
42
|
+
1: "Appropriations Received / short-term readiness",
|
|
43
|
+
2: "Statement of Budgetary Resources (SBR)",
|
|
44
|
+
3: "Mission-Critical Asset Existence & Completeness",
|
|
45
|
+
4: "Proprietary (full financial statement) audit",
|
|
46
|
+
}
|
|
47
|
+
# The FIAR Methodology organizes work as numbered KEY TASKS (e.g. 1.3.3 internal
|
|
48
|
+
# control testing, 1.4.5 KSD testing) that culminate in a management ASSERTION,
|
|
49
|
+
# an independent EXAMINATION/audit (IPA or DoD OIG), then SUSTAINMENT under OMB
|
|
50
|
+
# A-123 App. A. These five readiness stages summarize that flow for tracking.
|
|
51
|
+
PHASES = ("discover", "correct", "assert", "validate", "sustain")
|
|
52
|
+
# Assessable-unit business-process cycles the seed library covers.
|
|
53
|
+
CYCLES = {
|
|
54
|
+
"FBWT": "Fund Balance with Treasury",
|
|
55
|
+
"P2P": "Procure-to-Pay",
|
|
56
|
+
"PPE": "Property, Plant & Equipment",
|
|
57
|
+
"INV": "Inventory & Operating Materials",
|
|
58
|
+
"CIVPAY": "Civilian Pay",
|
|
59
|
+
"REIM": "Reimbursables / Order-to-Cash",
|
|
60
|
+
"FR": "Financial Reporting (Budget-to-Report)",
|
|
61
|
+
"ITGC": "IT General Controls",
|
|
62
|
+
}
|
|
63
|
+
# Control assessment outcomes.
|
|
64
|
+
CONTROL_STATUSES = ("not_tested", "effective", "deficient", "not_applicable")
|
|
65
|
+
# NFR severity, worst-first (GAO Green Book / A-123 deficiency taxonomy).
|
|
66
|
+
SEVERITIES = ("material_weakness", "significant_deficiency", "deficiency")
|
|
67
|
+
# CAP lifecycle.
|
|
68
|
+
CAP_STATUSES = ("open", "in_progress", "validated", "closed")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_STATUS_ALIASES = {
|
|
72
|
+
"pass": "effective", "passed": "effective", "compliant": "effective",
|
|
73
|
+
"effective": "effective",
|
|
74
|
+
"fail": "deficient", "failed": "deficient", "noncompliant": "deficient",
|
|
75
|
+
"deficient": "deficient",
|
|
76
|
+
"na": "not_applicable", "n_a": "not_applicable", "not_applicable": "not_applicable",
|
|
77
|
+
"open": "not_tested", "untested": "not_tested", "not_tested": "not_tested",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _norm_status(s: str | None) -> str:
|
|
82
|
+
return str(s or "").strip().lower().replace(" ", "_").replace("-", "_").replace("/", "_")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def canonical_status(s: str | None) -> str:
|
|
86
|
+
"""Lenient: map any input to a status, defaulting to not_tested. Use
|
|
87
|
+
recognized_status() to reject genuinely unknown input at a tool boundary."""
|
|
88
|
+
if not s:
|
|
89
|
+
return "not_tested"
|
|
90
|
+
k = _norm_status(s)
|
|
91
|
+
return _STATUS_ALIASES.get(k, k) if _STATUS_ALIASES.get(k, k) in CONTROL_STATUSES else "not_tested"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def recognized_status(s: str | None) -> bool:
|
|
95
|
+
"""True only if s is a known status or alias (not the lenient fallback)."""
|
|
96
|
+
k = _norm_status(s)
|
|
97
|
+
return k in _STATUS_ALIASES or k in CONTROL_STATUSES
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def canonical_assertion(a: str) -> str:
|
|
101
|
+
a0 = str(a).strip().upper()
|
|
102
|
+
if a0 in ASSERTIONS:
|
|
103
|
+
return a0
|
|
104
|
+
aliases = {"EXISTENCE": "E", "COMPLETENESS": "C", "ACCURACY": "V",
|
|
105
|
+
"VALUATION": "V", "VALUATION & ACCURACY": "V",
|
|
106
|
+
"PRESENTATION": "PD", "PRESENTATION & DISCLOSURE": "PD",
|
|
107
|
+
"DISCLOSURE": "PD", "RIGHTS": "RO", "RIGHTS & OBLIGATIONS": "RO",
|
|
108
|
+
"OBLIGATIONS": "RO"}
|
|
109
|
+
if a0 in aliases:
|
|
110
|
+
return aliases[a0]
|
|
111
|
+
rev = {v.upper(): k for k, v in ASSERTIONS.items()}
|
|
112
|
+
return rev.get(a0, a0)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── Data model ────────────────────────────────────────────────────────────
|
|
116
|
+
@dataclass
|
|
117
|
+
class Control:
|
|
118
|
+
"""One control objective within a cycle, mapped to assertions + KSDs."""
|
|
119
|
+
id: str
|
|
120
|
+
cycle: str # CYCLES key
|
|
121
|
+
objective: str # the control objective / risk it addresses
|
|
122
|
+
activity: str = "" # the control activity that should exist
|
|
123
|
+
assertions: list[str] = field(default_factory=list) # ASSERTIONS keys
|
|
124
|
+
fro: str = "" # Financial Reporting Objective it supports
|
|
125
|
+
ksds: list[str] = field(default_factory=list) # Key Supporting Documents
|
|
126
|
+
test_procedure: str = "" # how to test it
|
|
127
|
+
key_task: str = "" # FIAR Methodology key task (e.g. "1.3.3", "1.4.5")
|
|
128
|
+
itgc: bool = False # is this an IT General Control?
|
|
129
|
+
status: str = "not_tested"
|
|
130
|
+
evidence: str = "" # what was observed / KSDs examined
|
|
131
|
+
sample_size: int = 0
|
|
132
|
+
exceptions: int = 0 # test exceptions found in the sample
|
|
133
|
+
evidence_chain: dict = field(default_factory=dict) # EVIDENCE_CHAIN link -> evidence
|
|
134
|
+
|
|
135
|
+
def summary(self) -> str:
|
|
136
|
+
mark = {"effective": "✓", "deficient": "✗",
|
|
137
|
+
"not_applicable": "–", "not_tested": "·"}[self.status]
|
|
138
|
+
asrt = "/".join(self.assertions) or "-"
|
|
139
|
+
return f"{mark} {self.id} [{self.cycle}/{asrt}] {self.objective[:70]}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class Finding:
|
|
144
|
+
"""A Notice of Findings and Recommendations (NFR) — the audit finding form."""
|
|
145
|
+
id: str
|
|
146
|
+
control_id: str = ""
|
|
147
|
+
condition: str = "" # what is (the deficiency observed)
|
|
148
|
+
criteria: str = "" # what should be (policy/standard/GAAP)
|
|
149
|
+
cause: str = "" # why the gap exists (root cause)
|
|
150
|
+
effect: str = "" # impact on the assertion / balance
|
|
151
|
+
recommendation: str = ""
|
|
152
|
+
severity: str = "deficiency"
|
|
153
|
+
assertions: list[str] = field(default_factory=list)
|
|
154
|
+
status: str = "open"
|
|
155
|
+
|
|
156
|
+
def summary(self) -> str:
|
|
157
|
+
return f"[{self.severity}] {self.id} ({self.control_id or 'entity'}): {self.condition[:70]}"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class Cap:
|
|
162
|
+
"""A Corrective Action Plan addressing one NFR, with milestones."""
|
|
163
|
+
id: str
|
|
164
|
+
finding_id: str
|
|
165
|
+
root_cause: str = ""
|
|
166
|
+
action: str = ""
|
|
167
|
+
responsible: str = "" # responsible organization / POC
|
|
168
|
+
target_date: str = "" # ISO date
|
|
169
|
+
status: str = "open"
|
|
170
|
+
milestones: list[dict] = field(default_factory=list) # [{desc, due, done}]
|
|
171
|
+
|
|
172
|
+
def summary(self) -> str:
|
|
173
|
+
done = sum(1 for m in self.milestones if m.get("done"))
|
|
174
|
+
return (f"[{self.status}] {self.id} -> {self.finding_id}: {self.action[:60]} "
|
|
175
|
+
f"({done}/{len(self.milestones)} milestones, due {self.target_date or 'TBD'})")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Engagement:
|
|
179
|
+
"""One audit-readiness engagement, persisted as a single JSON file."""
|
|
180
|
+
|
|
181
|
+
def __init__(self, data: dict) -> None:
|
|
182
|
+
self._d = data
|
|
183
|
+
self._d.setdefault("controls", [])
|
|
184
|
+
self._d.setdefault("findings", [])
|
|
185
|
+
self._d.setdefault("caps", [])
|
|
186
|
+
|
|
187
|
+
# -- persistence --
|
|
188
|
+
@classmethod
|
|
189
|
+
def load(cls, path: str | Path) -> "Engagement":
|
|
190
|
+
return cls(json.loads(Path(path).read_text("utf-8")))
|
|
191
|
+
|
|
192
|
+
def save(self, path: str | Path) -> None:
|
|
193
|
+
Path(path).write_text(json.dumps(self._d, indent=2), "utf-8")
|
|
194
|
+
|
|
195
|
+
# -- header --
|
|
196
|
+
@property
|
|
197
|
+
def name(self) -> str: return self._d.get("engagement", "")
|
|
198
|
+
@property
|
|
199
|
+
def cycle(self) -> str: return self._d.get("assessable_unit", "")
|
|
200
|
+
@property
|
|
201
|
+
def phase(self) -> str: return self._d.get("phase", "discover")
|
|
202
|
+
@property
|
|
203
|
+
def wave(self) -> int: return int(self._d.get("wave", 4))
|
|
204
|
+
|
|
205
|
+
def set_phase(self, phase: str) -> bool:
|
|
206
|
+
if phase not in PHASES:
|
|
207
|
+
return False
|
|
208
|
+
self._d["phase"] = phase
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
# -- controls --
|
|
212
|
+
@property
|
|
213
|
+
def controls(self) -> list[Control]:
|
|
214
|
+
return [Control(**c) for c in self._d["controls"]]
|
|
215
|
+
|
|
216
|
+
def control(self, cid: str) -> Control | None:
|
|
217
|
+
for c in self._d["controls"]:
|
|
218
|
+
if c.get("id", "").lower() == cid.lower():
|
|
219
|
+
return Control(**c)
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def _raw_control(self, cid: str) -> dict | None:
|
|
223
|
+
for c in self._d["controls"]:
|
|
224
|
+
if c.get("id", "").lower() == cid.lower():
|
|
225
|
+
return c
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def assess(self, cid: str, *, status: str | None = None, evidence: str | None = None,
|
|
229
|
+
sample_size: int | None = None, exceptions: int | None = None,
|
|
230
|
+
chain: dict | None = None) -> bool:
|
|
231
|
+
c = self._raw_control(cid)
|
|
232
|
+
if c is None:
|
|
233
|
+
return False
|
|
234
|
+
if status is not None:
|
|
235
|
+
c["status"] = canonical_status(status)
|
|
236
|
+
if evidence is not None:
|
|
237
|
+
c["evidence"] = evidence
|
|
238
|
+
if sample_size is not None:
|
|
239
|
+
c["sample_size"] = int(sample_size)
|
|
240
|
+
if exceptions is not None:
|
|
241
|
+
c["exceptions"] = int(exceptions)
|
|
242
|
+
if chain is not None:
|
|
243
|
+
merged = dict(c.get("evidence_chain") or {})
|
|
244
|
+
merged.update({k: v for k, v in chain.items() if k in EVIDENCE_CHAIN})
|
|
245
|
+
c["evidence_chain"] = merged
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
def counts(self) -> dict:
|
|
249
|
+
c = {s: 0 for s in CONTROL_STATUSES}
|
|
250
|
+
for ctrl in self._d["controls"]:
|
|
251
|
+
c[canonical_status(ctrl.get("status"))] = c.get(canonical_status(ctrl.get("status")), 0) + 1
|
|
252
|
+
return c
|
|
253
|
+
|
|
254
|
+
# -- findings (NFRs) --
|
|
255
|
+
@property
|
|
256
|
+
def findings(self) -> list[Finding]:
|
|
257
|
+
return [Finding(**f) for f in self._d["findings"]]
|
|
258
|
+
|
|
259
|
+
def add_finding(self, **kw) -> Finding:
|
|
260
|
+
fid = kw.get("id") or f"NFR-{len(self._d['findings']) + 1:03d}"
|
|
261
|
+
kw["id"] = fid
|
|
262
|
+
kw["assertions"] = [canonical_assertion(a) for a in (kw.get("assertions") or [])]
|
|
263
|
+
if kw.get("severity") not in SEVERITIES:
|
|
264
|
+
kw["severity"] = "deficiency"
|
|
265
|
+
f = Finding(**{k: v for k, v in kw.items() if k in Finding.__dataclass_fields__})
|
|
266
|
+
self._d["findings"].append(asdict(f))
|
|
267
|
+
return f
|
|
268
|
+
|
|
269
|
+
# -- CAPs --
|
|
270
|
+
@property
|
|
271
|
+
def caps(self) -> list[Cap]:
|
|
272
|
+
return [Cap(**c) for c in self._d["caps"]]
|
|
273
|
+
|
|
274
|
+
def add_cap(self, **kw) -> Cap:
|
|
275
|
+
cid = kw.get("id") or f"CAP-{len(self._d['caps']) + 1:03d}"
|
|
276
|
+
kw["id"] = cid
|
|
277
|
+
c = Cap(**{k: v for k, v in kw.items() if k in Cap.__dataclass_fields__})
|
|
278
|
+
self._d["caps"].append(asdict(c))
|
|
279
|
+
return c
|
|
280
|
+
|
|
281
|
+
# -- readiness roll-up --
|
|
282
|
+
def readiness(self) -> dict:
|
|
283
|
+
cts = self.counts()
|
|
284
|
+
tested = cts["effective"] + cts["deficient"]
|
|
285
|
+
total_applicable = tested + cts["not_tested"]
|
|
286
|
+
pct_tested = round(100 * tested / total_applicable, 1) if total_applicable else 0.0
|
|
287
|
+
pct_effective = round(100 * cts["effective"] / tested, 1) if tested else 0.0
|
|
288
|
+
open_mw = sum(1 for f in self._d["findings"]
|
|
289
|
+
if f.get("severity") == "material_weakness" and f.get("status") != "closed")
|
|
290
|
+
open_caps = sum(1 for c in self._d["caps"] if c.get("status") != "closed")
|
|
291
|
+
return {
|
|
292
|
+
"phase": self.phase, "counts": cts,
|
|
293
|
+
"pct_tested": pct_tested, "pct_effective": pct_effective,
|
|
294
|
+
"open_findings": sum(1 for f in self._d["findings"] if f.get("status") != "closed"),
|
|
295
|
+
"open_material_weaknesses": open_mw, "open_caps": open_caps,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ── Evidence chain (the FIAR audit-trail every effective test must trace) ─
|
|
300
|
+
# A control test cannot support an assertion unless the evidence traces the full
|
|
301
|
+
# audit chain from the recorded population all the way to the FS assertion. This
|
|
302
|
+
# is the deterministic backbone that separates a real FIAR review from a checklist.
|
|
303
|
+
EVIDENCE_CHAIN = (
|
|
304
|
+
"population", # the recorded universe the sample was drawn from
|
|
305
|
+
"sample", # the specific item(s) selected
|
|
306
|
+
"source_transaction", # the transaction the item represents
|
|
307
|
+
"authorization", # who was authorized to commit/approve it
|
|
308
|
+
"supporting_document", # the KSD evidencing it
|
|
309
|
+
"system_posting", # where it posted in the feeder/accounting system
|
|
310
|
+
"gl_effect", # its effect in the general ledger
|
|
311
|
+
"assertion", # the FS assertion it ultimately supports
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def validate_chain(chain: dict | None) -> dict:
|
|
316
|
+
"""Given a chain dict {link: evidence}, report which links are present and
|
|
317
|
+
whether the trace is complete. A link counts as present only if it has a
|
|
318
|
+
non-empty value. Returns {complete, present, missing}."""
|
|
319
|
+
chain = chain or {}
|
|
320
|
+
present = [k for k in EVIDENCE_CHAIN if str(chain.get(k, "")).strip()]
|
|
321
|
+
missing = [k for k in EVIDENCE_CHAIN if k not in present]
|
|
322
|
+
return {"complete": not missing, "present": present, "missing": missing}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ── Reconciliation (a core FIAR substantive procedure, e.g. FBWT tie-out) ─
|
|
326
|
+
def reconcile(entity: float, source: float, *, tolerance: float = 0.0) -> dict:
|
|
327
|
+
"""Tie an entity balance to an authoritative source (e.g. Fund Balance with
|
|
328
|
+
Treasury vs Treasury's balance). Returns the difference and whether it clears
|
|
329
|
+
the tolerance — the shape of every FIAR reconciliation control."""
|
|
330
|
+
diff = round(float(entity) - float(source), 2)
|
|
331
|
+
return {
|
|
332
|
+
"entity": round(float(entity), 2), "source": round(float(source), 2),
|
|
333
|
+
"difference": diff, "abs_difference": abs(diff),
|
|
334
|
+
"tolerance": float(tolerance), "reconciled": abs(diff) <= float(tolerance),
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ── Seed control library — standard FIAR key control objectives ───────────
|
|
339
|
+
# A representative, extensible set of key control objectives per cycle, each
|
|
340
|
+
# mapped to the assertion(s) it supports and the KSDs that evidence it.
|
|
341
|
+
def _expand_assertions(codes: list[str]) -> list[str]:
|
|
342
|
+
"""Normalize assertion codes to the five keys, expanding the E&C pairing."""
|
|
343
|
+
out: list[str] = []
|
|
344
|
+
for x in codes:
|
|
345
|
+
xu = str(x).strip().upper()
|
|
346
|
+
if xu == "EC":
|
|
347
|
+
out += ["E", "C"]
|
|
348
|
+
elif xu == "VA":
|
|
349
|
+
out.append("V")
|
|
350
|
+
else:
|
|
351
|
+
out.append(canonical_assertion(xu))
|
|
352
|
+
seen: set[str] = set()
|
|
353
|
+
return [a for a in out if not (a in seen or seen.add(a))]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Most seed controls are internal-control tests (key task 1.3.3); KSD-existence
|
|
357
|
+
# and reconciliation tests are called out individually.
|
|
358
|
+
def _c(cid, cycle, objective, assertions, activity, ksds, test,
|
|
359
|
+
itgc=False, fro="", key_task="1.3.3") -> dict:
|
|
360
|
+
return asdict(Control(id=cid, cycle=cycle, objective=objective, activity=activity,
|
|
361
|
+
assertions=_expand_assertions(assertions), fro=fro, ksds=ksds,
|
|
362
|
+
test_procedure=test, key_task=key_task, itgc=itgc))
|
|
363
|
+
|
|
364
|
+
SEED_CONTROLS: dict[str, list[dict]] = {
|
|
365
|
+
"FBWT": [
|
|
366
|
+
_c("FBWT-01", "FBWT", "Monthly reconciliation of Fund Balance with Treasury to the Treasury balance is performed and reviewed.",
|
|
367
|
+
["EC", "VA"], "Preparer reconciles GL FBWT to TAS/BETC in GTAS/CARS; independent reviewer signs off.",
|
|
368
|
+
["Reconciliation workpaper", "GTAS/CARS statement", "GL trial balance"],
|
|
369
|
+
"Select a month; recompute the tie-out; confirm differences are researched and cleared and the review signature is present and timely."),
|
|
370
|
+
_c("FBWT-02", "FBWT", "Undistributed and in-transit disbursements/collections are researched and resolved timely.",
|
|
371
|
+
["EC", "VA"], "Aging of unmatched transactions; resolution within policy threshold.",
|
|
372
|
+
["UDO/in-transit aging report", "Resolution documentation"],
|
|
373
|
+
"Sample aged items; verify each was researched and cleared within the policy window."),
|
|
374
|
+
],
|
|
375
|
+
"P2P": [
|
|
376
|
+
_c("P2P-01", "P2P", "Obligations are recorded only for valid, authorized commitments supported by an obligating document.",
|
|
377
|
+
["EC", "RO"], "3-way check: obligating document exists, is signed by a warranted official, and matches the recorded obligation.",
|
|
378
|
+
["Obligating document (contract/PO/MIPR)", "Warrant/appointment", "GL obligation entry"],
|
|
379
|
+
"Sample recorded obligations; trace each to a signed obligating document by a warranted official for the recorded amount and period of availability."),
|
|
380
|
+
_c("P2P-02", "P2P", "Disbursements are made only for goods/services received and correctly matched (three-way match).",
|
|
381
|
+
["EC", "VA"], "Invoice matched to receiving report and obligating document before payment.",
|
|
382
|
+
["Invoice", "Receiving report / acceptance", "Obligating document", "Payment voucher"],
|
|
383
|
+
"Sample disbursements; verify a valid three-way match and that the paid amount agrees to the matched documents."),
|
|
384
|
+
_c("P2P-03", "P2P", "Purchases obligate against an available, correct appropriation (period, purpose, amount).",
|
|
385
|
+
["RO", "PD"], "Appropriation/line of accounting validated at commitment; ADA edits enforced.",
|
|
386
|
+
["Line of accounting", "Funds availability check"],
|
|
387
|
+
"Sample obligations; confirm the appropriation charged is correct as to purpose, time, and amount (Antideficiency edits present)."),
|
|
388
|
+
],
|
|
389
|
+
"PPE": [
|
|
390
|
+
_c("PPE-01", "PPE", "General PP&E recorded in the property system exists and is in the entity's possession/control.",
|
|
391
|
+
["EC", "RO"], "Periodic physical inventory reconciled to the accountable property system of record (APSR).",
|
|
392
|
+
["APSR listing", "Physical inventory count sheets", "Acquisition document"],
|
|
393
|
+
"Select recorded assets; physically observe existence; select floor assets; trace to the APSR (two-directional existence & completeness)."),
|
|
394
|
+
_c("PPE-02", "PPE", "PP&E is valued at capitalized acquisition cost and depreciated per policy.",
|
|
395
|
+
["VA"], "Capitalization threshold applied; depreciation method/life per policy; recomputation.",
|
|
396
|
+
["Acquisition/CLIN cost documentation", "Depreciation schedule"],
|
|
397
|
+
"Sample assets; recompute capitalized cost from KSDs and recompute depreciation; compare to the GL."),
|
|
398
|
+
],
|
|
399
|
+
"INV": [
|
|
400
|
+
_c("INV-01", "INV", "Inventory and operating materials & supplies recorded in the GL exist and are complete.",
|
|
401
|
+
["EC"], "Cyclic counts reconciled to the inventory system and GL.",
|
|
402
|
+
["Count sheets", "Inventory system extract", "GL balance"],
|
|
403
|
+
"Two-directional test: recorded-to-floor and floor-to-recorded; confirm count differences are adjusted."),
|
|
404
|
+
],
|
|
405
|
+
"CIVPAY": [
|
|
406
|
+
_c("CIVPAY-01", "CIVPAY", "Civilian pay is disbursed only to bona fide employees at authorized rates.",
|
|
407
|
+
["EC", "VA"], "Payroll reconciled to time & attendance and to authorized personnel actions (SF-50).",
|
|
408
|
+
["SF-50 personnel action", "Certified T&A", "Payroll register"],
|
|
409
|
+
"Sample pay transactions; trace to a valid SF-50 and certified T&A; recompute gross pay at the authorized rate."),
|
|
410
|
+
],
|
|
411
|
+
"REIM": [
|
|
412
|
+
_c("REIM-01", "REIM", "Reimbursable orders are supported by a valid agreement and revenue is recognized as earned.",
|
|
413
|
+
["EC", "RO", "VA"], "Order acceptance documented; earnings recognized on performance.",
|
|
414
|
+
["Reimbursable agreement (e.g. 7600A/B)", "Performance/delivery evidence"],
|
|
415
|
+
"Sample reimbursable revenue; trace to an accepted order and to evidence of performance for the recognized amount."),
|
|
416
|
+
],
|
|
417
|
+
"FR": [
|
|
418
|
+
_c("FR-01", "FR", "Journal vouchers (especially manual/top-side) are supported, reviewed, and approved.",
|
|
419
|
+
["VA", "PD"], "JV supported by documentation; prepared/reviewed by different people (SoD).",
|
|
420
|
+
["Journal voucher", "Supporting analysis", "Approval evidence"],
|
|
421
|
+
"Sample JVs (bias to manual/top-side); verify support, an independent review, and that preparer≠approver."),
|
|
422
|
+
_c("FR-02", "FR", "The trial balance ties to the financial statements and to GTAS with no unsupported abnormal balances.",
|
|
423
|
+
["EC", "VA", "PD"], "TB-to-FS crosswalk; GTAS edit/validation checks pass.",
|
|
424
|
+
["Crosswalk workpaper", "GTAS edit report"],
|
|
425
|
+
"Recompute the TB-to-FS tie-out; confirm GTAS validation/edit checks are clean or explained."),
|
|
426
|
+
],
|
|
427
|
+
"ITGC": [
|
|
428
|
+
_c("ITGC-01", "ITGC", "Access to financial systems is authorized, least-privilege, and periodically recertified.",
|
|
429
|
+
["EC", "VA"], "Provisioning approved; quarterly access recertification; timely deprovisioning.",
|
|
430
|
+
["Access request/approval", "Recertification listing", "Separation records"],
|
|
431
|
+
"Sample users; verify approved provisioning, recertification, and that separated users were disabled timely.", itgc=True),
|
|
432
|
+
_c("ITGC-02", "ITGC", "Segregation of duties prevents one user from both initiating and approving transactions.",
|
|
433
|
+
["EC", "RO"], "SoD ruleset enforced in the system; conflicts identified and mitigated.",
|
|
434
|
+
["SoD matrix", "Role assignments", "Conflict/mitigation log"],
|
|
435
|
+
"Obtain the role-to-user mapping; test for incompatible-duty combinations against the SoD matrix.", itgc=True),
|
|
436
|
+
_c("ITGC-03", "ITGC", "Changes to financial systems are authorized, tested, and approved before production.",
|
|
437
|
+
["VA"], "Change tickets approved; test evidence retained; migration by a separate role.",
|
|
438
|
+
["Change ticket", "Test evidence", "Approval/migration record"],
|
|
439
|
+
"Sample production changes; verify authorization, testing, approval, and migration SoD.", itgc=True),
|
|
440
|
+
],
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# Which FIAR Wave each cycle most naturally falls under.
|
|
445
|
+
CYCLE_WAVE = {"FR": 2, "REIM": 2, "P2P": 2, "CIVPAY": 2, # budgetary / SBR
|
|
446
|
+
"PPE": 3, "INV": 3, # asset existence & completeness
|
|
447
|
+
"FBWT": 4, "ITGC": 4} # proprietary / cross-cutting
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def new_engagement(name: str, cycle: str, wave: int | None = None) -> Engagement:
|
|
451
|
+
"""Create an engagement seeded with the key control objectives for a cycle
|
|
452
|
+
(or ALL cycles when cycle is '*' / 'all')."""
|
|
453
|
+
cyc = cycle.strip().upper()
|
|
454
|
+
if cyc in ("*", "ALL"):
|
|
455
|
+
controls = [dict(c) for lst in SEED_CONTROLS.values() for c in lst]
|
|
456
|
+
au = "All cycles"
|
|
457
|
+
w = wave or 4
|
|
458
|
+
else:
|
|
459
|
+
controls = [dict(c) for c in SEED_CONTROLS.get(cyc, [])]
|
|
460
|
+
au = CYCLES.get(cyc, cycle)
|
|
461
|
+
w = wave or CYCLE_WAVE.get(cyc, 4)
|
|
462
|
+
return Engagement({
|
|
463
|
+
"engagement": name, "assessable_unit": au, "cycle": cyc, "wave": w,
|
|
464
|
+
"phase": "discover", "created": date.today().isoformat(),
|
|
465
|
+
"controls": controls, "findings": [], "caps": [],
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ── CLI ───────────────────────────────────────────────────────────────────
|
|
470
|
+
def _main(argv: list[str]) -> int:
|
|
471
|
+
import argparse
|
|
472
|
+
p = argparse.ArgumentParser(prog="drydock.fiar", description="FIAR audit-readiness engine")
|
|
473
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
474
|
+
q = sub.add_parser("new"); q.add_argument("path"); q.add_argument("name"); q.add_argument("cycle")
|
|
475
|
+
q.add_argument("--wave", type=int, choices=[1, 2, 3, 4])
|
|
476
|
+
q = sub.add_parser("controls"); q.add_argument("path"); q.add_argument("--status")
|
|
477
|
+
q = sub.add_parser("control"); q.add_argument("path"); q.add_argument("id")
|
|
478
|
+
q = sub.add_parser("assess"); q.add_argument("path"); q.add_argument("id")
|
|
479
|
+
q.add_argument("--status"); q.add_argument("--evidence", default="")
|
|
480
|
+
q.add_argument("--sample", type=int); q.add_argument("--exceptions", type=int)
|
|
481
|
+
q = sub.add_parser("summary"); q.add_argument("path")
|
|
482
|
+
q = sub.add_parser("reconcile"); q.add_argument("entity", type=float)
|
|
483
|
+
q.add_argument("source", type=float); q.add_argument("--tolerance", type=float, default=0.0)
|
|
484
|
+
q = sub.add_parser("cycles")
|
|
485
|
+
a = p.parse_args(argv)
|
|
486
|
+
|
|
487
|
+
if a.cmd == "cycles":
|
|
488
|
+
for k, v in CYCLES.items():
|
|
489
|
+
print(f"{k:8} {v} ({len(SEED_CONTROLS.get(k, []))} controls)")
|
|
490
|
+
return 0
|
|
491
|
+
if a.cmd == "reconcile":
|
|
492
|
+
r = reconcile(a.entity, a.source, tolerance=a.tolerance)
|
|
493
|
+
print(json.dumps(r, indent=2)); return 0 if r["reconciled"] else 1
|
|
494
|
+
if a.cmd == "new":
|
|
495
|
+
eng = new_engagement(a.name, a.cycle, wave=a.wave); eng.save(a.path)
|
|
496
|
+
print(f"created {a.path}: {eng.name} / {eng.cycle} (Wave {eng.wave}) "
|
|
497
|
+
f"— {len(eng.controls)} controls"); return 0
|
|
498
|
+
|
|
499
|
+
eng = Engagement.load(a.path)
|
|
500
|
+
if a.cmd == "controls":
|
|
501
|
+
sf = canonical_status(a.status) if a.status else None
|
|
502
|
+
rows = [c for c in eng.controls if sf is None or c.status == sf]
|
|
503
|
+
print(f"{eng.name} [{eng.cycle}] Wave {eng.wave} phase={eng.phase} — " +
|
|
504
|
+
", ".join(f"{k}={v}" for k, v in eng.counts().items()))
|
|
505
|
+
for c in rows:
|
|
506
|
+
print(" " + c.summary())
|
|
507
|
+
elif a.cmd == "control":
|
|
508
|
+
c = eng.control(a.id)
|
|
509
|
+
if not c:
|
|
510
|
+
print(f"no control {a.id}"); return 1
|
|
511
|
+
print(f"{c.id} [{eng.cycle}] — {c.objective}\nAssertions: {', '.join(c.assertions)}\n"
|
|
512
|
+
f"Activity: {c.activity}\nKSDs: {', '.join(c.ksds)}\nTest: {c.test_procedure}\n"
|
|
513
|
+
f"Status: {c.status} sample={c.sample_size} exceptions={c.exceptions}\n"
|
|
514
|
+
f"Evidence: {c.evidence or '(none)'}")
|
|
515
|
+
elif a.cmd == "assess":
|
|
516
|
+
ok = eng.assess(a.id, status=a.status, evidence=a.evidence or None,
|
|
517
|
+
sample_size=a.sample, exceptions=a.exceptions)
|
|
518
|
+
if not ok:
|
|
519
|
+
print(f"no control {a.id}"); return 1
|
|
520
|
+
eng.save(a.path); print(f"✓ {a.id} -> {canonical_status(a.status)}")
|
|
521
|
+
elif a.cmd == "summary":
|
|
522
|
+
print(json.dumps(eng.readiness(), indent=2))
|
|
523
|
+
return 0
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
if __name__ == "__main__":
|
|
527
|
+
import sys
|
|
528
|
+
raise SystemExit(_main(sys.argv[1:]))
|