From 533e79ba959143f0459431a486bfb85c56c72ddc Mon Sep 17 00:00:00 2001 From: luisgulo Date: Fri, 24 Oct 2025 18:01:10 +0200 Subject: Releasing code version 1.8.0 --- LICENSE | 674 +++++++++++++++++++++ community_modules/ldap/ldap_ad.sh | 71 +++ community_modules/ldap/ldap_ad.tr.en | 7 + community_modules/ldap/ldap_ad.tr.es | 7 + community_modules/ldap/ldap_openldap.sh | 72 +++ community_modules/ldap/ldap_openldap.tr.en | 7 + community_modules/ldap/ldap_openldap.tr.es | 7 + community_modules/security/euvd_check.sh | 101 +++ community_modules/security/euvd_check.tr.en | 17 + community_modules/security/euvd_check.tr.es | 17 + community_modules/winremote/winremote_check.sh | 55 ++ community_modules/winremote/winremote_check.tr.en | 6 + community_modules/winremote/winremote_check.tr.es | 6 + community_modules/winremote/winremote_detect.sh | 65 ++ community_modules/winremote/winremote_detect.tr.en | 10 + community_modules/winremote/winremote_detect.tr.es | 10 + community_modules/winremote/winremote_exec.sh | 60 ++ community_modules/winremote/winremote_exec.tr.en | 6 + community_modules/winremote/winremote_exec.tr.es | 6 + .../winremote/winremote_exec_winrm.sh | 74 +++ .../winremote/winremote_exec_winrm.tr.en | 6 + .../winremote/winremote_exec_winrm.tr.es | 6 + core/inventory/groups.yaml | 9 + core/inventory/hosts.yaml | 17 + core/inventory/vars/all.yaml | 20 + core/lib/translate_msg.sh | 9 + core/modules/api.sh | 88 +++ core/modules/api.tr.en | 8 + core/modules/api.tr.es | 8 + core/modules/archive.sh | 113 ++++ core/modules/archive.tr.en | 14 + core/modules/archive.tr.es | 14 + core/modules/blockinfile.sh | 79 +++ core/modules/blockinfile.tr.en | 7 + core/modules/blockinfile.tr.es | 7 + core/modules/copy.sh | 65 ++ core/modules/copy.tr.en | 7 + core/modules/copy.tr.es | 7 + core/modules/cron.sh | 97 +++ core/modules/cron.tr.en | 12 + core/modules/cron.tr.es | 12 + core/modules/docker.sh | 81 +++ core/modules/docker.tr.en | 10 + core/modules/docker.tr.es | 10 + core/modules/download.sh | 64 ++ core/modules/download.tr.en | 7 + core/modules/download.tr.es | 7 + core/modules/echo.sh | 51 ++ core/modules/echo.tr.en | 2 + core/modules/echo.tr.es | 2 + core/modules/facts.sh | 121 ++++ core/modules/facts.tr.en | 5 + core/modules/facts.tr.es | 5 + core/modules/file.sh | 78 +++ core/modules/file.tr.en | 8 + core/modules/file.tr.es | 8 + core/modules/file_read.sh | 56 ++ core/modules/file_read.tr.en | 4 + core/modules/file_read.tr.es | 4 + core/modules/fs.sh | 91 +++ core/modules/fs.tr.en | 4 + core/modules/fs.tr.es | 4 + core/modules/git.sh | 88 +++ core/modules/git.tr.en | 7 + core/modules/git.tr.es | 7 + core/modules/groups.sh | 94 +++ core/modules/groups.tr.en | 15 + core/modules/groups.tr.es | 15 + core/modules/lineinfile.sh | 91 +++ core/modules/lineinfile.tr.en | 9 + core/modules/lineinfile.tr.es | 9 + core/modules/lookup.sh | 51 ++ core/modules/lookup.tr.en | 3 + core/modules/lookup.tr.es | 3 + core/modules/loop.sh | 130 ++++ core/modules/loop.tr.en | 7 + core/modules/loop.tr.es | 7 + core/modules/openssl.sh | 141 +++++ core/modules/openssl.tr.en | 20 + core/modules/openssl.tr.es | 20 + core/modules/package.sh | 132 ++++ core/modules/package.tr.en | 10 + core/modules/package.tr.es | 10 + core/modules/ping.sh | 55 ++ core/modules/ping.tr.en | 5 + core/modules/ping.tr.es | 5 + core/modules/replace.sh | 67 ++ core/modules/replace.tr.en | 5 + core/modules/replace.tr.es | 5 + core/modules/run.sh | 90 +++ core/modules/run.tr.en | 4 + core/modules/run.tr.es | 4 + core/modules/service.sh | 64 ++ core/modules/service.tr.en | 6 + core/modules/service.tr.es | 6 + core/modules/smtp_send.sh | 94 +++ core/modules/smtp_send.tr.en | 8 + core/modules/smtp_send.tr.es | 8 + core/modules/template.sh | 131 ++++ core/modules/template.tr.en | 5 + core/modules/template.tr.es | 5 + core/modules/user.tr.en | 15 + core/modules/user.tr.es | 15 + core/modules/users.sh | 106 ++++ core/modules/vault-remote.sh | 81 +++ core/modules/vault-remote.tr.en | 8 + core/modules/vault-remote.tr.es | 8 + core/modules/wait.sh | 39 ++ core/modules/wait.tr.en | 5 + core/modules/wait.tr.es | 5 + core/templates/.gitignore | 0 core/utils/eg.sh | 27 + core/utils/module-docgen.sh | 69 +++ core/utils/module-docgen.tr.en | 10 + core/utils/module-docgen.tr.es | 10 + core/utils/module-template.sh | 76 +++ core/utils/module-template.tr.en | 3 + core/utils/module-template.tr.es | 3 + core/utils/shflow-check.sh | 100 +++ core/utils/shflow-check.tr.en | 12 + core/utils/shflow-check.tr.es | 12 + core/utils/shflow-doc.sh | 63 ++ core/utils/shflow-doc.tr.en | 9 + core/utils/shflow-doc.tr.es | 9 + core/utils/shflow-ssh-init.sh | 74 +++ core/utils/shflow-ssh-init.tr.en | 12 + core/utils/shflow-ssh-init.tr.es | 12 + core/utils/shflow-trust.sh | 90 +++ core/utils/shflow-trust.tr.en | 14 + core/utils/shflow-trust.tr.es | 14 + core/utils/vault-init.sh | 97 +++ core/utils/vault-init.tr.en | 19 + core/utils/vault-init.tr.es | 19 + core/utils/vault_utils.sh | 41 ++ core/utils/vault_utils.tr.en | 2 + core/utils/vault_utils.tr.es | 2 + core/vault/.gitignore | 0 examples/api.yaml | 9 + examples/api_body_test.yaml | 62 ++ examples/api_get-json.yaml | 7 + examples/api_post-json.yaml | 8 + examples/api_post.yaml | 10 + examples/api_reqres.yaml | 11 + examples/api_soap-xml.yaml | 14 + examples/apt-update.yaml | 13 + examples/archive-compress-tar.gz.yaml | 9 + examples/archive-decompress-zip.yaml | 8 + examples/archive-extract-bootlog.yaml | 11 + examples/archive-extract-tar.gz.yaml | 9 + examples/basic_conditions.yaml | 15 + examples/block-in-file.yaml | 10 + examples/clone-and-configure.yaml | 24 + examples/cron_root.yaml | 46 ++ examples/deploy-nginx.yaml | 24 + examples/docker_and_ldapsearch.yaml | 31 + examples/docker_build-shflow.yaml | 8 + examples/docker_hello-world.yaml | 8 + examples/download.yaml | 9 + examples/echo_capture.yaml | 32 + examples/euvd_check.yaml | 17 + examples/facts_key-value.yaml | 6 + examples/facts_markdown.yaml | 8 + examples/file_read_apache-config.yaml | 10 + examples/fs-copy.yaml | 6 + examples/fs-delete-multiple.yaml | 5 + examples/fs-delete.yaml | 5 + examples/fs-move-multiple.yaml | 6 + examples/fs-move.yaml | 6 + examples/fs-rename.yaml | 6 + examples/fs-truncate-multiple.yaml | 5 + examples/fs-truncate.yaml | 5 + examples/full-stack.yaml | 0 examples/groups_minimal.yaml | 6 + examples/install-tools.yaml | 14 + examples/ldap_search.yaml | 24 + examples/line-in-file.yaml | 8 + examples/log-shflow.yaml | 15 + examples/lookup_password.yaml | 18 + examples/loop.yaml | 9 + examples/loop_cartesian-values.yaml | 9 + examples/loop_fail-fast.yaml | 10 + examples/loop_key-value.yaml | 10 + examples/loop_matrix.yaml | 14 + examples/ls.yaml | 13 + examples/minimal.yaml | 15 + examples/openssl_autogen.yaml | 46 ++ examples/ping.yaml | 7 + examples/remote-ping.yaml | 8 + examples/remote_user.yaml | 9 + examples/replace-string.yaml | 8 + examples/smtp_send_test.yaml | 12 + examples/template.yaml | 8 + examples/update_datetime.yaml | 43 ++ examples/update_so.yaml | 10 + examples/update_so_security.yaml | 8 + examples/user_create.yaml | 12 + examples/user_delete.yaml | 12 + examples/user_modify.yaml | 12 + examples/user_test.yaml | 32 + examples/useradd.yaml | 8 + examples/vault-demo.yaml | 7 + examples/vault-sync.yaml | 23 + examples/wait.yaml | 5 + examples/winremote_check_test.yaml | 11 + examples/winremote_detect.yaml | 8 + examples/winremote_exec.yaml | 10 + examples/winremote_exec_winrm.yml | 9 + install.sh | 129 ++++ shflow-logo.ascii | 18 + shflow-logo.png | Bin 0 -> 126318 bytes shflow.sh | 308 ++++++++++ shflow.tr.en | 32 + shflow.tr.es | 32 + user_modules/.gitignore | 0 vault.sh | 146 +++++ vault.tr.en | 13 + vault.tr.es | 13 + 217 files changed, 6649 insertions(+) create mode 100644 LICENSE create mode 100644 community_modules/ldap/ldap_ad.sh create mode 100644 community_modules/ldap/ldap_ad.tr.en create mode 100644 community_modules/ldap/ldap_ad.tr.es create mode 100644 community_modules/ldap/ldap_openldap.sh create mode 100644 community_modules/ldap/ldap_openldap.tr.en create mode 100644 community_modules/ldap/ldap_openldap.tr.es create mode 100644 community_modules/security/euvd_check.sh create mode 100644 community_modules/security/euvd_check.tr.en create mode 100644 community_modules/security/euvd_check.tr.es create mode 100644 community_modules/winremote/winremote_check.sh create mode 100644 community_modules/winremote/winremote_check.tr.en create mode 100644 community_modules/winremote/winremote_check.tr.es create mode 100644 community_modules/winremote/winremote_detect.sh create mode 100644 community_modules/winremote/winremote_detect.tr.en create mode 100644 community_modules/winremote/winremote_detect.tr.es create mode 100644 community_modules/winremote/winremote_exec.sh create mode 100644 community_modules/winremote/winremote_exec.tr.en create mode 100644 community_modules/winremote/winremote_exec.tr.es create mode 100644 community_modules/winremote/winremote_exec_winrm.sh create mode 100644 community_modules/winremote/winremote_exec_winrm.tr.en create mode 100644 community_modules/winremote/winremote_exec_winrm.tr.es create mode 100644 core/inventory/groups.yaml create mode 100644 core/inventory/hosts.yaml create mode 100644 core/inventory/vars/all.yaml create mode 100644 core/lib/translate_msg.sh create mode 100644 core/modules/api.sh create mode 100644 core/modules/api.tr.en create mode 100644 core/modules/api.tr.es create mode 100644 core/modules/archive.sh create mode 100644 core/modules/archive.tr.en create mode 100644 core/modules/archive.tr.es create mode 100644 core/modules/blockinfile.sh create mode 100644 core/modules/blockinfile.tr.en create mode 100644 core/modules/blockinfile.tr.es create mode 100644 core/modules/copy.sh create mode 100644 core/modules/copy.tr.en create mode 100644 core/modules/copy.tr.es create mode 100644 core/modules/cron.sh create mode 100644 core/modules/cron.tr.en create mode 100644 core/modules/cron.tr.es create mode 100644 core/modules/docker.sh create mode 100644 core/modules/docker.tr.en create mode 100644 core/modules/docker.tr.es create mode 100644 core/modules/download.sh create mode 100644 core/modules/download.tr.en create mode 100644 core/modules/download.tr.es create mode 100644 core/modules/echo.sh create mode 100644 core/modules/echo.tr.en create mode 100644 core/modules/echo.tr.es create mode 100644 core/modules/facts.sh create mode 100644 core/modules/facts.tr.en create mode 100644 core/modules/facts.tr.es create mode 100644 core/modules/file.sh create mode 100644 core/modules/file.tr.en create mode 100644 core/modules/file.tr.es create mode 100644 core/modules/file_read.sh create mode 100644 core/modules/file_read.tr.en create mode 100644 core/modules/file_read.tr.es create mode 100644 core/modules/fs.sh create mode 100644 core/modules/fs.tr.en create mode 100644 core/modules/fs.tr.es create mode 100644 core/modules/git.sh create mode 100644 core/modules/git.tr.en create mode 100644 core/modules/git.tr.es create mode 100644 core/modules/groups.sh create mode 100644 core/modules/groups.tr.en create mode 100644 core/modules/groups.tr.es create mode 100644 core/modules/lineinfile.sh create mode 100644 core/modules/lineinfile.tr.en create mode 100644 core/modules/lineinfile.tr.es create mode 100644 core/modules/lookup.sh create mode 100644 core/modules/lookup.tr.en create mode 100644 core/modules/lookup.tr.es create mode 100644 core/modules/loop.sh create mode 100644 core/modules/loop.tr.en create mode 100644 core/modules/loop.tr.es create mode 100644 core/modules/openssl.sh create mode 100644 core/modules/openssl.tr.en create mode 100644 core/modules/openssl.tr.es create mode 100644 core/modules/package.sh create mode 100644 core/modules/package.tr.en create mode 100644 core/modules/package.tr.es create mode 100644 core/modules/ping.sh create mode 100644 core/modules/ping.tr.en create mode 100644 core/modules/ping.tr.es create mode 100644 core/modules/replace.sh create mode 100644 core/modules/replace.tr.en create mode 100644 core/modules/replace.tr.es create mode 100644 core/modules/run.sh create mode 100644 core/modules/run.tr.en create mode 100644 core/modules/run.tr.es create mode 100644 core/modules/service.sh create mode 100644 core/modules/service.tr.en create mode 100644 core/modules/service.tr.es create mode 100644 core/modules/smtp_send.sh create mode 100644 core/modules/smtp_send.tr.en create mode 100644 core/modules/smtp_send.tr.es create mode 100644 core/modules/template.sh create mode 100644 core/modules/template.tr.en create mode 100644 core/modules/template.tr.es create mode 100644 core/modules/user.tr.en create mode 100644 core/modules/user.tr.es create mode 100644 core/modules/users.sh create mode 100644 core/modules/vault-remote.sh create mode 100644 core/modules/vault-remote.tr.en create mode 100644 core/modules/vault-remote.tr.es create mode 100644 core/modules/wait.sh create mode 100644 core/modules/wait.tr.en create mode 100644 core/modules/wait.tr.es create mode 100644 core/templates/.gitignore create mode 100755 core/utils/eg.sh create mode 100755 core/utils/module-docgen.sh create mode 100644 core/utils/module-docgen.tr.en create mode 100644 core/utils/module-docgen.tr.es create mode 100755 core/utils/module-template.sh create mode 100644 core/utils/module-template.tr.en create mode 100644 core/utils/module-template.tr.es create mode 100755 core/utils/shflow-check.sh create mode 100644 core/utils/shflow-check.tr.en create mode 100644 core/utils/shflow-check.tr.es create mode 100755 core/utils/shflow-doc.sh create mode 100644 core/utils/shflow-doc.tr.en create mode 100644 core/utils/shflow-doc.tr.es create mode 100755 core/utils/shflow-ssh-init.sh create mode 100644 core/utils/shflow-ssh-init.tr.en create mode 100644 core/utils/shflow-ssh-init.tr.es create mode 100755 core/utils/shflow-trust.sh create mode 100644 core/utils/shflow-trust.tr.en create mode 100644 core/utils/shflow-trust.tr.es create mode 100755 core/utils/vault-init.sh create mode 100644 core/utils/vault-init.tr.en create mode 100644 core/utils/vault-init.tr.es create mode 100755 core/utils/vault_utils.sh create mode 100644 core/utils/vault_utils.tr.en create mode 100644 core/utils/vault_utils.tr.es create mode 100644 core/vault/.gitignore create mode 100644 examples/api.yaml create mode 100644 examples/api_body_test.yaml create mode 100644 examples/api_get-json.yaml create mode 100644 examples/api_post-json.yaml create mode 100644 examples/api_post.yaml create mode 100644 examples/api_reqres.yaml create mode 100644 examples/api_soap-xml.yaml create mode 100644 examples/apt-update.yaml create mode 100644 examples/archive-compress-tar.gz.yaml create mode 100644 examples/archive-decompress-zip.yaml create mode 100644 examples/archive-extract-bootlog.yaml create mode 100644 examples/archive-extract-tar.gz.yaml create mode 100644 examples/basic_conditions.yaml create mode 100644 examples/block-in-file.yaml create mode 100644 examples/clone-and-configure.yaml create mode 100644 examples/cron_root.yaml create mode 100644 examples/deploy-nginx.yaml create mode 100644 examples/docker_and_ldapsearch.yaml create mode 100644 examples/docker_build-shflow.yaml create mode 100644 examples/docker_hello-world.yaml create mode 100644 examples/download.yaml create mode 100644 examples/echo_capture.yaml create mode 100644 examples/euvd_check.yaml create mode 100644 examples/facts_key-value.yaml create mode 100644 examples/facts_markdown.yaml create mode 100644 examples/file_read_apache-config.yaml create mode 100644 examples/fs-copy.yaml create mode 100644 examples/fs-delete-multiple.yaml create mode 100644 examples/fs-delete.yaml create mode 100644 examples/fs-move-multiple.yaml create mode 100644 examples/fs-move.yaml create mode 100644 examples/fs-rename.yaml create mode 100644 examples/fs-truncate-multiple.yaml create mode 100644 examples/fs-truncate.yaml create mode 100644 examples/full-stack.yaml create mode 100644 examples/groups_minimal.yaml create mode 100644 examples/install-tools.yaml create mode 100644 examples/ldap_search.yaml create mode 100644 examples/line-in-file.yaml create mode 100644 examples/log-shflow.yaml create mode 100644 examples/lookup_password.yaml create mode 100644 examples/loop.yaml create mode 100644 examples/loop_cartesian-values.yaml create mode 100644 examples/loop_fail-fast.yaml create mode 100644 examples/loop_key-value.yaml create mode 100644 examples/loop_matrix.yaml create mode 100644 examples/ls.yaml create mode 100644 examples/minimal.yaml create mode 100644 examples/openssl_autogen.yaml create mode 100644 examples/ping.yaml create mode 100644 examples/remote-ping.yaml create mode 100644 examples/remote_user.yaml create mode 100644 examples/replace-string.yaml create mode 100644 examples/smtp_send_test.yaml create mode 100644 examples/template.yaml create mode 100644 examples/update_datetime.yaml create mode 100644 examples/update_so.yaml create mode 100644 examples/update_so_security.yaml create mode 100644 examples/user_create.yaml create mode 100644 examples/user_delete.yaml create mode 100644 examples/user_modify.yaml create mode 100644 examples/user_test.yaml create mode 100644 examples/useradd.yaml create mode 100644 examples/vault-demo.yaml create mode 100644 examples/vault-sync.yaml create mode 100644 examples/wait.yaml create mode 100644 examples/winremote_check_test.yaml create mode 100644 examples/winremote_detect.yaml create mode 100644 examples/winremote_exec.yaml create mode 100644 examples/winremote_exec_winrm.yml create mode 100755 install.sh create mode 100644 shflow-logo.ascii create mode 100644 shflow-logo.png create mode 100755 shflow.sh create mode 100644 shflow.tr.en create mode 100644 shflow.tr.es create mode 100644 user_modules/.gitignore create mode 100755 vault.sh create mode 100644 vault.tr.en create mode 100644 vault.tr.es diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/community_modules/ldap/ldap_ad.sh b/community_modules/ldap/ldap_ad.sh new file mode 100644 index 0000000..361b7fb --- /dev/null +++ b/community_modules/ldap/ldap_ad.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Module: ldap_ad +# Description: Realiza búsquedas filtradas en servidores Active Directory usando ldapsearch +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: ldapsearch + +ldap_ad_task() { + local host="$1" + shift + + check_dependencies_ldap_ad || return 1 + + local state="" server="" port="389" base_dn="" filter="" attributes="" bind_dn="" password="" + for arg in "$@"; do + case "$arg" in + state=*) state="${arg#state=}" ;; + server=*) server="${arg#server=}" ;; + port=*) port="${arg#port=}" ;; + base_dn=*) base_dn="${arg#base_dn=}" ;; + filter=*) filter="${arg#filter=}" ;; + attributes=*) attributes="${arg#attributes=}" ;; + bind_dn=*) bind_dn="${arg#bind_dn=}" ;; + password=*) password="${arg#password=}" ;; + esac + done + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ldap_ad.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ "$state" != "search" ]]; then + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + fi + + if [[ -z "$server" || -z "$base_dn" || -z "$filter" ]]; then + echo "${tr[missing_args]:-❌ [ldap_ad] Faltan argumentos obligatorios: server, base_dn, filter}" + return 1 + fi + + echo "$(render_msg "${tr[connecting]}" "server=$server" "port=$port")" + local cmd=(ldapsearch -LLL -H "$server" -p "$port" -b "$base_dn" "$filter") + [[ -n "$bind_dn" && -n "$password" ]] && cmd=(-D "$bind_dn" -w "$password" "${cmd[@]}") + [[ -n "$attributes" ]] && IFS=',' read -ra attr_list <<< "$attributes" && cmd+=("${attr_list[@]}") + + if "${cmd[@]}" 2>/tmp/ldap_ad_error.log | grep -E '^(dn:|cn:|mail:|sAMAccountName:)' ; then + echo "${tr[success]:-✅ [ldap_ad] Búsqueda completada con éxito}" + else + echo "${tr[no_results]:-⚠️ [ldap_ad] No se encontraron resultados o hubo un error}" + cat /tmp/ldap_ad_error.log + return 1 + fi +} + +check_dependencies_ldap_ad() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ldap_ad.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v ldapsearch &>/dev/null; then + echo "${tr[missing_dep]:-❌ [ldap_ad] El comando 'ldapsearch' no está disponible}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [ldap_ad] Dependencias OK}" + return 0 +} diff --git a/community_modules/ldap/ldap_ad.tr.en b/community_modules/ldap/ldap_ad.tr.en new file mode 100644 index 0000000..55214ae --- /dev/null +++ b/community_modules/ldap/ldap_ad.tr.en @@ -0,0 +1,7 @@ +unsupported_state=❌ [ldap_ad] Unsupported state: '{state}'. Only 'search' is allowed +missing_args=❌ [ldap_ad] Missing required arguments: server, base_dn, filter +connecting=🔍 [ldap_ad] Connecting to {server}:{port}... +success=✅ [ldap_ad] Search completed successfully +no_results=⚠️ [ldap_ad] No results found or an error occurred +missing_dep=❌ [ldap_ad] 'ldapsearch' command is not available +deps_ok=✅ [ldap_ad] Dependencies OK diff --git a/community_modules/ldap/ldap_ad.tr.es b/community_modules/ldap/ldap_ad.tr.es new file mode 100644 index 0000000..93dca53 --- /dev/null +++ b/community_modules/ldap/ldap_ad.tr.es @@ -0,0 +1,7 @@ +unsupported_state=❌ [ldap_ad] Estado no soportado: '{state}'. Solo se permite 'search' +missing_args=❌ [ldap_ad] Faltan argumentos obligatorios: server, base_dn, filter +connecting=🔍 [ldap_ad] Conectando a {server}:{port}... +success=✅ [ldap_ad] Búsqueda completada con éxito +no_results=⚠️ [ldap_ad] No se encontraron resultados o hubo un error +missing_dep=❌ [ldap_ad] El comando 'ldapsearch' no está disponible +deps_ok=✅ [ldap_ad] Dependencias OK diff --git a/community_modules/ldap/ldap_openldap.sh b/community_modules/ldap/ldap_openldap.sh new file mode 100644 index 0000000..bfe8a85 --- /dev/null +++ b/community_modules/ldap/ldap_openldap.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Module: ldap_openldap +# Description: Realiza búsquedas filtradas en servidores OpenLDAP usando ldapsearch +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: ldapsearch + +ldap_openldap_task() { + local host="$1" + shift + + check_dependencies_ldap_openldap || return 1 + + local state="" server="" port="389" base_dn="" filter="" attributes="" bind_dn="" password="" + for arg in "$@"; do + case "$arg" in + state=*) state="${arg#state=}" ;; + server=*) server="${arg#server=}" ;; + port=*) port="${arg#port=}" ;; + base_dn=*) base_dn="${arg#base_dn=}" ;; + filter=*) filter="${arg#filter=}" ;; + attributes=*) attributes="${arg#attributes=}" ;; + bind_dn=*) bind_dn="${arg#bind_dn=}" ;; + password=*) password="${arg#password=}" ;; + esac + done + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ldap_openldap.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ "$state" != "search" ]]; then + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + fi + + if [[ -z "$server" || -z "$base_dn" || -z "$filter" ]]; then + echo "${tr[missing_args]:-❌ [ldap_openldap] Faltan argumentos obligatorios: server, base_dn, filter}" + return 1 + fi + + echo "$(render_msg "${tr[connecting]}" "server=$server" "port=$port")" + local cmd=(ldapsearch -x -H "$server:$port") + [[ -n "$bind_dn" && -n "$password" ]] && cmd+=(-D "$bind_dn" -w "$password") + cmd+=(-b "$base_dn" "$filter") + [[ -n "$attributes" ]] && IFS=',' read -ra attr_list <<< "$attributes" && cmd+=("${attr_list[@]}") + + if "${cmd[@]}" 2>/tmp/ldap_error.log | grep -E '^(dn:|cn:|mail:|uid:)' ; then + echo "${tr[success]:-✅ [ldap_openldap] Búsqueda completada con éxito}" + else + echo "${tr[no_results]:-⚠️ [ldap_openldap] No se encontraron resultados o hubo un error}" + cat /tmp/ldap_error.log + return 1 + fi +} + +check_dependencies_ldap_openldap() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ldap_openldap.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v ldapsearch &>/dev/null; then + echo "${tr[missing_dep]:-❌ [ldap_openldap] El comando 'ldapsearch' no está disponible}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [ldap_openldap] Dependencias OK}" + return 0 +} diff --git a/community_modules/ldap/ldap_openldap.tr.en b/community_modules/ldap/ldap_openldap.tr.en new file mode 100644 index 0000000..9868fb5 --- /dev/null +++ b/community_modules/ldap/ldap_openldap.tr.en @@ -0,0 +1,7 @@ +unsupported_state=❌ [ldap_openldap] Unsupported state: '{state}'. Only 'search' is allowed +missing_args=❌ [ldap_openldap] Missing required arguments: server, base_dn, filter +connecting=🔍 [ldap_openldap] Connecting to {server}:{port}... +success=✅ [ldap_openldap] Search completed successfully +no_results=⚠️ [ldap_openldap] No results found or an error occurred +missing_dep=❌ [ldap_openldap] 'ldapsearch' command is not available +deps_ok=✅ [ldap_openldap] Dependencies OK diff --git a/community_modules/ldap/ldap_openldap.tr.es b/community_modules/ldap/ldap_openldap.tr.es new file mode 100644 index 0000000..62a649b --- /dev/null +++ b/community_modules/ldap/ldap_openldap.tr.es @@ -0,0 +1,7 @@ +unsupported_state=❌ [ldap_openldap] Estado no soportado: '{state}'. Solo se permite 'search' +missing_args=❌ [ldap_openldap] Faltan argumentos obligatorios: server, base_dn, filter +connecting=🔍 [ldap_openldap] Conectando a {server}:{port}... +success=✅ [ldap_openldap] Búsqueda completada con éxito +no_results=⚠️ [ldap_openldap] No se encontraron resultados o hubo un error +missing_dep=❌ [ldap_openldap] El comando 'ldapsearch' no está disponible +deps_ok=✅ [ldap_openldap] Dependencias OK diff --git a/community_modules/security/euvd_check.sh b/community_modules/security/euvd_check.sh new file mode 100644 index 0000000..2205a42 --- /dev/null +++ b/community_modules/security/euvd_check.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Module: euvd_check +# Description: Verifica si un host remoto está afectado por una vulnerabilidad EUVD consultando la base europea ENISA +# License: GPLv3 +# Author: Luis GuLo +# Version: 0.6.0 +# Dependencies: curl, jq, ssh, dpkg o rpm + +euvd_check_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local enisa_id="${args[enisa_id]}" + local package="${args[package]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/euvd_check.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$enisa_id" || -z "$package" ]]; then + echo "${tr[missing_args]:-❌ [euvd_check] Faltan argumentos: enisa_id y package son obligatorios.}" + return 1 + fi + + echolog 1 "$(render_msg "${tr[start]}" "id=$enisa_id" "package=$package" "host=$host")" + + local pkg_cmd="" + if ssh "$host" "command -v dpkg" &>/dev/null; then + pkg_cmd="dpkg -s" + echolog 1 "${tr[detected_dpkg]:-🔧 Gestor de paquetes detectado: dpkg}" + elif ssh "$host" "command -v rpm" &>/dev/null; then + pkg_cmd="rpm -q" + echolog 1 "${tr[detected_rpm]:-🔧 Gestor de paquetes detectado: rpm}" + else + echo "${tr[no_pkg]:-❌ [euvd_check] No se detectó gestor de paquetes compatible en el host}" + return 1 + fi + + local version_cmd="$pkg_cmd $package" + [[ "$become" = "true" ]] && version_cmd="sudo $version_cmd" + + local version + version=$(ssh "$host" "$version_cmd" 2>/dev/null | grep -E 'Version|version|^'"$package" | head -n1 | awk '{print $2}') + + if [[ -z "$version" ]]; then + echolog 1 "$(render_msg "${tr[version_fail]}" "package=$package" "host=$host")" + return 1 + fi + + echolog 1 "$(render_msg "${tr[version_ok]}" "version=$version")" + + local enisa_url="https://euvdservices.enisa.europa.eu/api/enisaid?id=$enisa_id" + echolog 1 "$(render_msg "${tr[query_enisa]}" "id=$enisa_id")" + local response + response=$(curl -s -X GET "$enisa_url") + + if ! echo "$response" | jq -e .description &>/dev/null; then + echolog 1 "$(render_msg "${tr[invalid_response]}" "id=$enisa_id")" + echolog 1 "$(render_msg "${tr[response_trunc]}" "snippet=$(echo "$response" | head -c 120 | tr '\n' ' ')")" + return 1 + fi + + local score desc aliases + score=$(echo "$response" | jq -r '.baseScore // empty') + desc=$(echo "$response" | jq -r '.description // empty') + aliases=$(echo "$response" | jq -r '.aliases[]?') + + [[ -n "$score" ]] && echolog 1 "$(render_msg "${tr[score]}" "score=$score")" + echolog 2 "$(render_msg "${tr[desc]}" "desc=$desc")" + [[ -n "$aliases" ]] && echolog 1 "$(render_msg "${tr[aliases]}" "aliases=$aliases")" + + if echo "$desc" | grep -iq "$package" && echo "$desc" | grep -q "$version"; then + echo "$(render_msg "${tr[vulnerable]}" "host=$host" "id=$enisa_id")" + return 1 + else + echo "$(render_msg "${tr[safe]}" "host=$host" "id=$enisa_id")" + return 0 + fi +} + +check_dependencies_euvd_check() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/euvd_check.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + for cmd in ssh curl jq; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_dep]}" "cmd=$cmd")" + return 1 + fi + done + echo "${tr[deps_ok]:-✅ [euvd_check] ssh, curl y jq disponibles.}" + return 0 +} diff --git a/community_modules/security/euvd_check.tr.en b/community_modules/security/euvd_check.tr.en new file mode 100644 index 0000000..ae95dd0 --- /dev/null +++ b/community_modules/security/euvd_check.tr.en @@ -0,0 +1,17 @@ +missing_args=❌ [euvd_check] Missing arguments: enisa_id and package are required. +start=🧬 [euvd_check] Checking {id} in package '{package}' on {host}... +detected_dpkg=🔧 Package manager detected: dpkg +detected_rpm=🔧 Package manager detected: rpm +no_pkg=❌ [euvd_check] No compatible package manager detected on host +version_fail=⚠️ [euvd_check] Could not detect installed version of '{package}' on {host}. +version_ok=🔍 Installed version: {version} +query_enisa=🌐 Querying ENISA for {id}... +invalid_response=⚠️ [euvd_check] ENISA response does not contain valid data for {id}. +response_trunc=🔍 Response (truncated): {snippet} +score=📊 CVSS Score: {score} +desc=📝 Description: {desc} +aliases=🔗 Aliases: {aliases} +vulnerable=❌ [euvd_check] Host {host} is vulnerable to {id} +safe=✅ [euvd_check] Host {host} does not appear affected by {id} +missing_dep=❌ [euvd_check] Command '{cmd}' is not available +deps_ok=✅ [euvd_check] ssh, curl and jq are available. diff --git a/community_modules/security/euvd_check.tr.es b/community_modules/security/euvd_check.tr.es new file mode 100644 index 0000000..85e0315 --- /dev/null +++ b/community_modules/security/euvd_check.tr.es @@ -0,0 +1,17 @@ +missing_args=❌ [euvd_check] Faltan argumentos: enisa_id y package son obligatorios. +start=🧬 [euvd_check] Verificando {id} en paquete '{package}' en {host}... +detected_dpkg=🔧 Gestor de paquetes detectado: dpkg +detected_rpm=🔧 Gestor de paquetes detectado: rpm +no_pkg=❌ [euvd_check] No se detectó gestor de paquetes compatible en el host +version_fail=⚠️ [euvd_check] No se pudo detectar la versión instalada de '{package}' en {host}. +version_ok=🔍 Versión instalada: {version} +query_enisa=🌐 Consultando ENISA para {id}... +invalid_response=⚠️ [euvd_check] La respuesta de ENISA no contiene datos válidos para {id}. +response_trunc=🔍 Respuesta (truncada): {snippet} +score=📊 Puntuación CVSS: {score} +desc=📝 Descripción: {desc} +aliases=🔗 Alias: {aliases} +vulnerable=❌ [euvd_check] Host {host} está vulnerable a {id} +safe=✅ [euvd_check] Host {host} no parece afectado por {id} +missing_dep=❌ [euvd_check] El comando '{cmd}' no está disponible +deps_ok=✅ [euvd_check] ssh, curl y jq disponibles. diff --git a/community_modules/winremote/winremote_check.sh b/community_modules/winremote/winremote_check.sh new file mode 100644 index 0000000..4f0d608 --- /dev/null +++ b/community_modules/winremote/winremote_check.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Module: winremote_check +# Description: Verifica conectividad y ejecución remota básica en equipos Windows mediante SSH y PowerShell +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: ssh, powershell (en el host remoto) + +winremote_check_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local winuser="${args[winuser]}" + local winpassword="${args[winpassword]}" + local port="${args[port]:-22}" + local command="Write-Output 'Conexión establecida desde ShFlow'" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_check.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$host" || -z "$winuser" || -z "$winpassword" ]]; then + echo "${tr[missing_args]:-❌ [winremote_check] Parámetros incompletos. Se requiere host, winuser y winpassword.}" + return 1 + fi + + [[ "$host" == *@* ]] && host=$(echo "$host" | awk -F '@' '{print $2}') + + echo "$(render_msg "${tr[start]}" "host=$host")" + + if sshpass -p "$winpassword" ssh -o PreferredAuthentications=password -o StrictHostKeyChecking=no -p "$port" "$winuser@$host" powershell -Command "\"$command\"" &>/dev/null; then + echo "$(render_msg "${tr[success]}" "host=$host")" + return 0 + else + echo "$(render_msg "${tr[fail]}" "host=$host")" + return 1 + fi +} + +check_dependencies_winremote_check() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_check.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_ssh]:-❌ [winremote_check] ssh no está disponible.}" + return 1 + fi + echo "${tr[ssh_ok]:-✅ [winremote_check] ssh disponible.}" + return 0 +} diff --git a/community_modules/winremote/winremote_check.tr.en b/community_modules/winremote/winremote_check.tr.en new file mode 100644 index 0000000..3af94a8 --- /dev/null +++ b/community_modules/winremote/winremote_check.tr.en @@ -0,0 +1,6 @@ +missing_args=❌ [winremote_check] Missing parameters. host, winuser and winpassword are required. +start=🖥️ [winremote_check] Checking remote access to {host}... +success=✅ [winremote_check] Remote connection and execution OK on {host} +fail=❌ [winremote_check] Connection or execution failed on {host} +missing_ssh=❌ [winremote_check] ssh is not available. +ssh_ok=✅ [winremote_check] ssh is available. diff --git a/community_modules/winremote/winremote_check.tr.es b/community_modules/winremote/winremote_check.tr.es new file mode 100644 index 0000000..b80f33b --- /dev/null +++ b/community_modules/winremote/winremote_check.tr.es @@ -0,0 +1,6 @@ +missing_args=❌ [winremote_check] Parámetros incompletos. Se requiere host, winuser y winpassword. +start=🖥️ [winremote_check] Verificando acceso remoto a {host}... +success=✅ [winremote_check] Conexión y ejecución remota OK en {host} +fail=❌ [winremote_check] Fallo de conexión o ejecución en {host} +missing_ssh=❌ [winremote_check] ssh no está disponible. +ssh_ok=✅ [winremote_check] ssh disponible. diff --git a/community_modules/winremote/winremote_detect.sh b/community_modules/winremote/winremote_detect.sh new file mode 100644 index 0000000..4477acd --- /dev/null +++ b/community_modules/winremote/winremote_detect.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Module: winremote_detect +# Description: Detecta si un host Windows tiene habilitado SSH, WinRM, ambos o ninguno +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: nc, curl, pwsh (opcional) + +winremote_detect_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local ssh_port="${args[ssh_port]:-22}" + local winrm_port="${args[winrm_port]:-5985}" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_detect.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + [[ "$host" == *@* ]] && host=$(echo "$host" | awk -F '@' '{print $2}') + + echo "$(render_msg "${tr[start]}" "host=$host")" + + local ssh_status="${tr[ssh_off]:-❌ SSH no disponible}" + local winrm_status="${tr[winrm_off]:-❌ WinRM no disponible}" + + if nc -z -w2 "$host" "$ssh_port" &>/dev/null; then + ssh_status="$(render_msg "${tr[ssh_on]}" "port=$ssh_port")" + fi + + if curl -s -o /dev/null -w "%{http_code}" --connect-timeout 2 "http://$host:$winrm_port/wsman" | grep -q "405"; then + winrm_status="$(render_msg "${tr[winrm_on]}" "port=$winrm_port")" + fi + + echo " $ssh_status" + echo " $winrm_status" + + if [[ "$ssh_status" == *✅* && "$winrm_status" == *✅* ]]; then + echo "$(render_msg "${tr[both]}" "host=$host")" + return 0 + elif [[ "$ssh_status" == *✅* || "$winrm_status" == *✅* ]]; then + echo "${tr[one]:-🟡 Uno de los protocolos está disponible}" + return 0 + else + echo "$(render_msg "${tr[none]}" "host=$host")" + return 1 + fi +} + +check_dependencies_winremote_detect() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_detect.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v nc &> /dev/null || ! command -v curl &> /dev/null; then + echo "${tr[missing_deps]:-❌ [winremote_detect] nc o curl no están disponibles.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [winremote_detect] nc y curl disponibles.}" + return 0 +} diff --git a/community_modules/winremote/winremote_detect.tr.en b/community_modules/winremote/winremote_detect.tr.en new file mode 100644 index 0000000..986e44f --- /dev/null +++ b/community_modules/winremote/winremote_detect.tr.en @@ -0,0 +1,10 @@ +start=🔍 [winremote_detect] Checking connectivity with {host}... +ssh_on=✅ SSH enabled (port {port}) +ssh_off=❌ SSH not available +winrm_on=✅ WinRM enabled (port {port}) +winrm_off=❌ WinRM not available +both=🟢 Both protocols available on {host} +one=🟡 One protocol is available +none=🔴 No remote protocol detected on {host} +missing_deps=❌ [winremote_detect] nc or curl are not available. +deps_ok=✅ [winremote_detect] nc and curl are available. diff --git a/community_modules/winremote/winremote_detect.tr.es b/community_modules/winremote/winremote_detect.tr.es new file mode 100644 index 0000000..530a034 --- /dev/null +++ b/community_modules/winremote/winremote_detect.tr.es @@ -0,0 +1,10 @@ +start=🔍 [winremote_detect] Analizando conectividad con {host}... +ssh_on=✅ SSH habilitado (puerto {port}) +ssh_off=❌ SSH no disponible +winrm_on=✅ WinRM habilitado (puerto {port}) +winrm_off=❌ WinRM no disponible +both=🟢 Ambos protocolos disponibles en {host} +one=🟡 Uno de los protocolos está disponible +none=🔴 Ningún protocolo remoto detectado en {host} +missing_deps=❌ [winremote_detect] nc o curl no están disponibles. +deps_ok=✅ [winremote_detect] nc y curl disponibles. diff --git a/community_modules/winremote/winremote_exec.sh b/community_modules/winremote/winremote_exec.sh new file mode 100644 index 0000000..79bf413 --- /dev/null +++ b/community_modules/winremote/winremote_exec.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Module: winremote_exec +# Description: Ejecuta comandos PowerShell en un host Windows remoto vía SSH +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: sshpass, ssh + +winremote_exec_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local winuser="${args[winuser]}" + local winpassword="${args[winpassword]}" + local port="${args[port]:-22}" + local command="${args[command]}" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_exec.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$host" || -z "$winuser" || -z "$winpassword" || -z "$command" ]]; then + echo "${tr[missing_args]:-❌ [winremote_exec] Parámetros incompletos. Se requiere host, winuser, winpassword y command.}" + return 1 + fi + + [[ "$host" == *@* ]] && host=$(echo "$host" | awk -F '@' '{print $2}') + local safe_command=$(printf "%q" "$command") + + echo "$(render_msg "${tr[start]}" "host=$host" "port=$port" "user=$winuser")" + + sshpass -p "$winpassword" ssh -o PreferredAuthentications=password -o StrictHostKeyChecking=no -p "$port" "$winuser@$host" \ + "powershell -Command \"$safe_command\"" + + local exit_code=$? + if [[ $exit_code -eq 0 ]]; then + echo "${tr[success]:-✅ [winremote_exec] Comando ejecutado correctamente.}" + return 0 + else + echo "$(render_msg "${tr[fail]}" "code=$exit_code")" + return $exit_code + fi +} + +check_dependencies_winremote_exec() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_exec.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v sshpass &> /dev/null || ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [winremote_exec] sshpass o ssh no están disponibles.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [winremote_exec] sshpass y ssh disponibles.}" + return 0 +} diff --git a/community_modules/winremote/winremote_exec.tr.en b/community_modules/winremote/winremote_exec.tr.en new file mode 100644 index 0000000..e1fa189 --- /dev/null +++ b/community_modules/winremote/winremote_exec.tr.en @@ -0,0 +1,6 @@ +missing_args=❌ [winremote_exec] Missing parameters. host, winuser, winpassword and command are required. +start=🔧 [winremote_exec] Executing remote command on {host}:{port} as {user}... +success=✅ [winremote_exec] Command executed successfully. +fail=❌ [winremote_exec] Error executing command (code {code}). +missing_deps=❌ [winremote_exec] sshpass or ssh are not available. +deps_ok=✅ [winremote_exec] sshpass and ssh are available. diff --git a/community_modules/winremote/winremote_exec.tr.es b/community_modules/winremote/winremote_exec.tr.es new file mode 100644 index 0000000..1a52f10 --- /dev/null +++ b/community_modules/winremote/winremote_exec.tr.es @@ -0,0 +1,6 @@ +missing_args=❌ [winremote_exec] Parámetros incompletos. Se requiere host, winuser, winpassword y command. +start=🔧 [winremote_exec] Ejecutando comando remoto en {host}:{port} como {user}... +success=✅ [winremote_exec] Comando ejecutado correctamente. +fail=❌ [winremote_exec] Error al ejecutar el comando (código {code}). +missing_deps=❌ [winremote_exec] sshpass o ssh no están disponibles. +deps_ok=✅ [winremote_exec] sshpass y ssh disponibles. diff --git a/community_modules/winremote/winremote_exec_winrm.sh b/community_modules/winremote/winremote_exec_winrm.sh new file mode 100644 index 0000000..8d4e1bd --- /dev/null +++ b/community_modules/winremote/winremote_exec_winrm.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Module: winremote_exec_winrm +# Description: Ejecuta comandos en un host Windows remoto vía WSMan (WinRM) desde Linux +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.3.0 +# Dependencies: wsman + +winremote_exec_winrm_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local winuser="${args[winuser]}" + local winpassword="${args[winpassword]}" + local port="${args[port]:-5985}" + local command="${args[command]}" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_exec_winrm.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$host" || -z "$winuser" || -z "$winpassword" || -z "$command" ]]; then + echo "${tr[missing_args]:-❌ [winremote_exec_winrm] Parámetros incompletos. Se requiere host, winuser, winpassword y command.}" + return 1 + fi + + [[ "$host" == *@* ]] && host=$(echo "$host" | awk -F '@' '{print $2}') + + local xml_file=$(mktemp --suffix=.xml) + trap '[[ -n "$xml_file" && -f "$xml_file" ]] && rm -f "$xml_file"' EXIT + + cat > "$xml_file" < + ${command} + +EOF + + echo "$(render_msg "${tr[start]}" "host=$host" "port=$port" "user=$winuser")" + + wsman invoke http://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Process \ + -a Create \ + -h "$host" \ + -P "$port" \ + -u "$winuser" \ + -p "$winpassword" \ + -y basic \ + -J "$xml_file" + + local exit_code=$? + if [[ $exit_code -eq 0 ]]; then + echo "${tr[success]:-✅ [winremote_exec_winrm] Comando ejecutado correctamente vía WSMan.}" + return 0 + else + echo "$(render_msg "${tr[fail]}" "code=$exit_code")" + return $exit_code + fi +} + +check_dependencies_winremote_exec_winrm() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/winremote_exec_winrm.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v wsman &> /dev/null; then + echo "${tr[missing_wsman]:-❌ [winremote_exec_winrm] El cliente 'wsman' no está disponible.}" + return 1 + fi + echo "${tr[wsman_ok]:-✅ [winremote_exec_winrm] Cliente 'wsman' disponible.}" + return 0 +} diff --git a/community_modules/winremote/winremote_exec_winrm.tr.en b/community_modules/winremote/winremote_exec_winrm.tr.en new file mode 100644 index 0000000..ddb3ebe --- /dev/null +++ b/community_modules/winremote/winremote_exec_winrm.tr.en @@ -0,0 +1,6 @@ +missing_args=❌ [winremote_exec_winrm] Missing parameters. host, winuser, winpassword and command are required. +start=🔧 [winremote_exec_winrm] Executing remote command on {host}:{port} as {user}... +success=✅ [winremote_exec_winrm] Command executed successfully via WSMan. +fail=❌ [winremote_exec_winrm] Error executing command (code {code}). +missing_wsman=❌ [winremote_exec_winrm] 'wsman' client is not available. +wsman_ok=✅ [winremote_exec_winrm] 'wsman' client is available. diff --git a/community_modules/winremote/winremote_exec_winrm.tr.es b/community_modules/winremote/winremote_exec_winrm.tr.es new file mode 100644 index 0000000..51fe754 --- /dev/null +++ b/community_modules/winremote/winremote_exec_winrm.tr.es @@ -0,0 +1,6 @@ +missing_args=❌ [winremote_exec_winrm] Parámetros incompletos. Se requiere host, winuser, winpassword y command. +start=🔧 [winremote_exec_winrm] Ejecutando comando remoto en {host}:{port} como {user}... +success=✅ [winremote_exec_winrm] Comando ejecutado correctamente vía WSMan. +fail=❌ [winremote_exec_winrm] Error al ejecutar el comando (código {code}). +missing_wsman=❌ [winremote_exec_winrm] El cliente 'wsman' no está disponible. +wsman_ok=✅ [winremote_exec_winrm] Cliente 'wsman' disponible. diff --git a/core/inventory/groups.yaml b/core/inventory/groups.yaml new file mode 100644 index 0000000..52dae9b --- /dev/null +++ b/core/inventory/groups.yaml @@ -0,0 +1,9 @@ +# groups.yaml — Definición de grupos en ShFlow +# Este archivo permite describir grupos, asignar etiquetas y metadatos. +# Sintaxis: +# groups: +# nombre_grupo: +# description: +# tags: [tag1, tag2, ...] + +groups: {} diff --git a/core/inventory/hosts.yaml b/core/inventory/hosts.yaml new file mode 100644 index 0000000..faafa09 --- /dev/null +++ b/core/inventory/hosts.yaml @@ -0,0 +1,17 @@ +# hosts.yaml — Inventario principal de ShFlow +# Este archivo define los hosts y su pertenencia a grupos. +# Sintaxis: +# all: +# hosts: +# nombre_host: +# ansible_host: +# become: +# : +# children: +# nombre_grupo: +# hosts: +# nombre_host: + +all: + hosts: {} + children: {} diff --git a/core/inventory/vars/all.yaml b/core/inventory/vars/all.yaml new file mode 100644 index 0000000..e0c5b8f --- /dev/null +++ b/core/inventory/vars/all.yaml @@ -0,0 +1,20 @@ +# all.yaml — Variables globales para todos los hosts en ShFlow +# Este archivo define valores comunes que se aplican a todos los hosts del inventario. +# Sintaxis: +# clave: valor +# Las variables aquí definidas pueden ser sobrescritas por vars de grupo o de host. + +language: es +timezone: Europe/Madrid +ntp_servers: + - 0.europe.pool.ntp.org + - 1.europe.pool.ntp.org +default_packages: + - curl + - vim + - bash-completion +ssh_port: 22 +become: true +env: production +vault_enabled: true +vault_rotation_interval: 30d diff --git a/core/lib/translate_msg.sh b/core/lib/translate_msg.sh new file mode 100644 index 0000000..3a1e4a9 --- /dev/null +++ b/core/lib/translate_msg.sh @@ -0,0 +1,9 @@ +render_msg() { + local template="$1"; shift + for pair in "$@"; do + local key="${pair%%=*}" + local val="${pair#*=}" + template="${template//\{$key\}/$val}" + done + echo "$template" +} diff --git a/core/modules/api.sh b/core/modules/api.sh new file mode 100644 index 0000000..a58f81a --- /dev/null +++ b/core/modules/api.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Module: api +# Description: Cliente declarativo para APIs REST y SOAP (GET, POST, PUT, DELETE, SOAP) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: curl, jq, xmllint + +api_task() { + local host="$1"; shift + declare -A args + local headers=() + local method="" body="" url="" output="" parse="" + + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}" + case "$key" in + headers) IFS=',' read -r -a headers <<< "$value" ;; + body) body="$value" ;; + url) url="$value" ;; + method) method="${value,,}" ;; + output) output="$value" ;; + parse) parse="${value,,}" ;; + esac + done + + [[ -z "$method" ]] && method="get" + [[ "$method" == "get" ]] && method="GET" + [[ "$method" == "post" ]] && method="POST" + [[ "$method" == "soap" ]] && method="POST" + + local header_args="" + for h in "${headers[@]}"; do header_args+=" -H \"$h\""; done + + local curl_cmd="curl -sSL -X $method \"$url\"$header_args" + [[ -n "$body" ]] && curl_cmd+=" --data-raw '$body'" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/api.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + echo "$(render_msg "${tr[start]}" "method=$method" "url=$url")" + [[ "$DEBUG" == "true" ]] && echo "$(render_msg "${tr[debug_cmd]}" "cmd=$curl_cmd")" + [[ "$DEBUG" == "true" && -n "$body" ]] && echo -e "$(render_msg "${tr[debug_body]}" "body=$body")" + + local response + if [[ "$host" == "localhost" ]]; then + response=$(eval "$curl_cmd") + else + response=$(ssh "$host" "$curl_cmd") + fi + + if [[ -n "$output" ]]; then + echo "$response" > "$output" + echo "$(render_msg "${tr[saved]}" "output=$output")" + fi + + case "$parse" in + json) + echo "$response" | jq '.' 2>/dev/null || echo "${tr[json_fail]:-⚠️ [api] No se pudo parsear como JSON}" + ;; + xml) + echo "$response" | xmllint --format - 2>/dev/null || { + echo "${tr[xml_fail]:-⚠️ [api] No se pudo parsear como XML}" + echo "$response" + } + ;; + *) echo "$response" ;; + esac +} + +check_dependencies_api() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/api.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + for cmd in curl jq xmllint; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")" + else + echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")" + fi + done + return 0 +} diff --git a/core/modules/api.tr.en b/core/modules/api.tr.en new file mode 100644 index 0000000..fce15f6 --- /dev/null +++ b/core/modules/api.tr.en @@ -0,0 +1,8 @@ +start=🌐 [api] Executing {method} → {url} +debug_cmd=🔍 Actual command: {cmd} +debug_body=📦 [api] Body sent:\n{body} +saved=💾 [api] Response saved to: {output} +json_fail=⚠️ [api] Failed to parse as JSON +xml_fail=⚠️ [api] Failed to parse as XML +missing_cmd=⚠️ [api] '{cmd}' not available locally. Assuming it exists on the remote host. +cmd_ok=✅ [api] '{cmd}' available locally. diff --git a/core/modules/api.tr.es b/core/modules/api.tr.es new file mode 100644 index 0000000..6e75c04 --- /dev/null +++ b/core/modules/api.tr.es @@ -0,0 +1,8 @@ +start=🌐 [api] Ejecutando {method} → {url} +debug_cmd=🔍 Comando real: {cmd} +debug_body=📦 [api] Cuerpo enviado:\n{body} +saved=💾 [api] Respuesta guardada en: {output} +json_fail=⚠️ [api] No se pudo parsear como JSON +xml_fail=⚠️ [api] No se pudo parsear como XML +missing_cmd=⚠️ [api] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto. +cmd_ok=✅ [api] '{cmd}' disponible localmente. diff --git a/core/modules/archive.sh b/core/modules/archive.sh new file mode 100644 index 0000000..4459bfd --- /dev/null +++ b/core/modules/archive.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Module: archive +# Description: Comprime, descomprime y extrae archivos en remoto (tar, zip, gzip, bzip2) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.6.0 +# Dependencies: ssh, tar, gzip, bzip2, zip, unzip + +archive_task() { + local host="$1"; shift + declare -A args + local files=() + + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}" + [[ "$key" == "files" ]] && IFS=',' read -r -a files <<< "$value" || args["$key"]="$value" + done + + local action="${args[action]}" + local format="${args[format]:-tar}" + local become="${args[become]:-false}" + local prefix="" + [[ "$become" == "true" ]] && prefix="sudo" + + local output="" archive="" dest="" + case "$action" in + compress) output="${args[output]}" ;; + decompress|extract) archive="${args[archive]}"; dest="${args[dest]:-$(dirname "$archive")}" ;; + esac + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/archive.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ "$action" == "extract" || "$action" == "decompress" ]]; then + ssh "$host" "[ -d '$dest' ] || $prefix mkdir -p '$dest'" || { + echo "$(render_msg "${tr[mkdir_fail]}" "dest=$dest")" + return 1 + } + fi + + case "$action" in + compress) + case "$format" in + tar) + ssh "$host" "$prefix tar -czf '$output' ${files[*]}" && echo "$(render_msg "${tr[compressed_tar]}" "output=$output")" + ;; + zip) + ssh "$host" "$prefix zip -r '$output' ${files[*]}" && echo "$(render_msg "${tr[compressed_zip]}" "output=$output")" + ;; + gzip) + for file in "${files[@]}"; do + ssh "$host" "$prefix gzip -f '$file'" && echo "$(render_msg "${tr[compressed_gzip]}" "file=$file")" + done + ;; + bzip2) + for file in "${files[@]}"; do + ssh "$host" "$prefix bzip2 -f '$file'" && echo "$(render_msg "${tr[compressed_bzip2]}" "file=$file")" + done + ;; + *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;; + esac + ;; + decompress) + case "$format" in + gzip) + ssh "$host" "$prefix gunzip -f '$archive'" && echo "$(render_msg "${tr[decompressed_gzip]}" "archive=$archive")" + ;; + bzip2) + ssh "$host" "$prefix bunzip2 -f '$archive'" && echo "$(render_msg "${tr[decompressed_bzip2]}" "archive=$archive")" + ;; + zip) + ssh "$host" "$prefix unzip -o '$archive' -d '$dest'" && echo "$(render_msg "${tr[decompressed_zip]}" "dest=$dest")" + ;; + *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;; + esac + ;; + extract) + case "$format" in + tar) + if [[ ${#files[@]} -gt 0 ]]; then + ssh "$host" "$prefix tar -xzf '$archive' -C '$dest' ${files[*]}" && echo "$(render_msg "${tr[extracted_tar]}" "dest=$dest")" + else + ssh "$host" "$prefix tar -xzf '$archive' -C '$dest'" && echo "$(render_msg "${tr[extracted_tar]}" "dest=$dest")" + fi + ;; + zip) + ssh "$host" "$prefix unzip -o '$archive' -d '$dest'" && echo "$(render_msg "${tr[extracted_zip]}" "dest=$dest")" + ;; + *) echo "$(render_msg "${tr[unsupported_format]}" "format=$format")"; return 1 ;; + esac + ;; + *) echo "$(render_msg "${tr[unsupported_action]}" "action=$action")"; return 1 ;; + esac +} + +check_dependencies_archive() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/archive.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + for cmd in ssh tar gzip bzip2 zip unzip; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")" + else + echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")" + fi + done + return 0 +} diff --git a/core/modules/archive.tr.en b/core/modules/archive.tr.en new file mode 100644 index 0000000..5528413 --- /dev/null +++ b/core/modules/archive.tr.en @@ -0,0 +1,14 @@ +mkdir_fail=❌ [archive] Failed to create destination directory: {dest} +compressed_tar=📦 [archive] Compressed as TAR: {output} +compressed_zip=📦 [archive] Compressed as ZIP: {output} +compressed_gzip=📦 [archive] GZIP: {file}.gz +compressed_bzip2=📦 [archive] BZIP2: {file}.bz2 +decompressed_gzip=📂 [archive] Decompressed GZIP: {archive} +decompressed_bzip2=📂 [archive] Decompressed BZIP2: {archive} +decompressed_zip=📂 [archive] Decompressed ZIP into: {dest} +extracted_tar=📂 [archive] Extracted TAR into: {dest} +extracted_zip=📂 [archive] Extracted ZIP into: {dest} +unsupported_format=❌ [archive] Unsupported format '{format}' +unsupported_action=❌ [archive] Unsupported action '{action}' +missing_cmd=⚠️ [archive] '{cmd}' not available locally. Assuming it exists on the remote host. +cmd_ok=✅ [archive] '{cmd}' available locally. diff --git a/core/modules/archive.tr.es b/core/modules/archive.tr.es new file mode 100644 index 0000000..86764fd --- /dev/null +++ b/core/modules/archive.tr.es @@ -0,0 +1,14 @@ +mkdir_fail=❌ [archive] No se pudo crear el directorio destino: {dest} +compressed_tar=📦 [archive] Comprimido en TAR: {output} +compressed_zip=📦 [archive] Comprimido en ZIP: {output} +compressed_gzip=📦 [archive] GZIP: {file}.gz +compressed_bzip2=📦 [archive] BZIP2: {file}.bz2 +decompressed_gzip=📂 [archive] Descomprimido GZIP: {archive} +decompressed_bzip2=📂 [archive] Descomprimido BZIP2: {archive} +decompressed_zip=📂 [archive] Descomprimido ZIP en: {dest} +extracted_tar=📂 [archive] Extraído TAR en: {dest} +extracted_zip=📂 [archive] Extraído ZIP en: {dest} +unsupported_format=❌ [archive] Formato '{format}' no soportado +unsupported_action=❌ [archive] Acción '{action}' no soportada +missing_cmd=⚠️ [archive] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto. +cmd_ok=✅ [archive] '{cmd}' disponible localmente. diff --git a/core/modules/blockinfile.sh b/core/modules/blockinfile.sh new file mode 100644 index 0000000..e42f5d8 --- /dev/null +++ b/core/modules/blockinfile.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Module: blockinfile +# Description: Inserta o actualiza bloques de texto delimitados en archivos +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: grep, sed, tee, awk + +blockinfile_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value" + done + + local path="${args[path]}" + local block="${args[block]}" + local marker="${args[marker]:-SHFLOW}" + local create="${args[create]:-true}" + local backup="${args[backup]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + local start="# BEGIN $marker" + local end="# END $marker" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/blockinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ ! -f "$path" ]]; then + if [[ "$create" == "true" ]]; then + echo "$(render_msg "${tr[creating]}" "path=$path")" + touch "$path" + else + echo "$(render_msg "${tr[missing_file]}" "path=$path")" + return 1 + fi + fi + + if [[ "$backup" == "true" ]]; then + cp "$path" "$path.bak" + echo "$(render_msg "${tr[backup]}" "path=$path")" + fi + + if grep -q "$start" "$path"; then + echo "$(render_msg "${tr[replacing]}" "marker=$marker")" + $prefix sed -i "/$start/,/$end/d" "$path" + fi + + echo "$(render_msg "${tr[inserting]}" "marker=$marker")" + { + echo "$start" + echo "$block" + echo "$end" + } | $prefix tee -a "$path" > /dev/null +} + +check_dependencies_blockinfile() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/blockinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + local missing=() + for cmd in grep sed tee awk; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [blockinfile] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/blockinfile.tr.en b/core/modules/blockinfile.tr.en new file mode 100644 index 0000000..70a7711 --- /dev/null +++ b/core/modules/blockinfile.tr.en @@ -0,0 +1,7 @@ +creating=📄 [blockinfile] Creating file: {path} +missing_file=❌ [blockinfile] File does not exist and create=false +backup=📦 Backup created: {path}.bak +replacing=🔁 [blockinfile] Replacing existing block with marker '{marker}' +inserting=➕ [blockinfile] Inserting delimited block with marker '{marker}' +missing_deps=❌ [blockinfile] Missing dependencies: {cmds} +deps_ok=✅ [blockinfile] All dependencies are available diff --git a/core/modules/blockinfile.tr.es b/core/modules/blockinfile.tr.es new file mode 100644 index 0000000..98df5c7 --- /dev/null +++ b/core/modules/blockinfile.tr.es @@ -0,0 +1,7 @@ +creating=📄 [blockinfile] Creando archivo: {path} +missing_file=❌ [blockinfile] El archivo no existe y create=false +backup=📦 Copia de seguridad creada: {path}.bak +replacing=🔁 [blockinfile] Reemplazando bloque existente con marcador '{marker}' +inserting=➕ [blockinfile] Insertando bloque delimitado con marcador '{marker}' +missing_deps=❌ [blockinfile] Dependencias faltantes: {cmds} +deps_ok=✅ [blockinfile] Todas las dependencias están disponibles diff --git a/core/modules/copy.sh b/core/modules/copy.sh new file mode 100644 index 0000000..b46d9e5 --- /dev/null +++ b/core/modules/copy.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Module: copy +# Description: Copia archivos locales al host remoto usando scp +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: scp, ssh + +copy_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local src="${args[src]}" + local dest="${args[dest]}" + local mode="${args[mode]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/copy.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$src" || -z "$dest" ]]; then + echo "${tr[missing_args]:-❌ [copy] Faltan parámetros: src y dest son obligatorios}" + return 1 + fi + + echo "$(render_msg "${tr[copying]}" "src=$src" "host=$host")" + scp "$src" "$host:/tmp/shflow_tmpfile" || { + echo "$(render_msg "${tr[scp_fail]}" "src=$src" "host=$host")" + return 1 + } + + echo "$(render_msg "${tr[moving]}" "dest=$dest")" + ssh "$host" "$prefix mv /tmp/shflow_tmpfile '$dest' && $prefix chmod $mode '$dest'" && \ + echo "$(render_msg "${tr[done]}" "dest=$dest")" +} + +check_dependencies_copy() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/copy.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + local missing=() + for cmd in scp ssh; do + command -v "$cmd" &> /dev/null || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [copy] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/copy.tr.en b/core/modules/copy.tr.en new file mode 100644 index 0000000..dcace81 --- /dev/null +++ b/core/modules/copy.tr.en @@ -0,0 +1,7 @@ +missing_args=❌ [copy] Missing parameters: src and dest are required +copying=📤 [copy] Copying '{src}' to host '{host}'... +scp_fail=❌ [copy] Failed to copy '{src}' to host '{host}' +moving=📦 [copy] Moving file to final destination: '{dest}' +done=✅ [copy] File successfully installed at '{dest}' +missing_deps=❌ [copy] Missing dependencies: {cmds} +deps_ok=✅ [copy] All dependencies are available diff --git a/core/modules/copy.tr.es b/core/modules/copy.tr.es new file mode 100644 index 0000000..9b5f934 --- /dev/null +++ b/core/modules/copy.tr.es @@ -0,0 +1,7 @@ +missing_args=❌ [copy] Faltan parámetros: src y dest son obligatorios +copying=📤 [copy] Copiando '{src}' al host '{host}'... +scp_fail=❌ [copy] Falló la copia de '{src}' al host '{host}' +moving=📦 [copy] Moviendo archivo a destino final: '{dest}' +done=✅ [copy] Archivo instalado correctamente en '{dest}' +missing_deps=❌ [copy] Dependencias faltantes: {cmds} +deps_ok=✅ [copy] Todas las dependencias están disponibles diff --git a/core/modules/cron.sh b/core/modules/cron.sh new file mode 100644 index 0000000..c1c4533 --- /dev/null +++ b/core/modules/cron.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Module: cron +# Description: Gestiona entradas de cron para usuarios del sistema (crear, modificar, eliminar, listar) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: bash, crontab, grep, id, sudo + +cron_task() { + local host="$1"; shift + local alias="" user="" state="" schedule="" command="" + for arg in "$@"; do + case "$arg" in + alias=*) alias="${arg#alias=}" ;; + user=*) user="${arg#user=}" ;; + state=*) state="${arg#state=}" ;; + schedule=*) schedule="${arg#schedule=}" ;; + command=*) command="${arg#command=}" ;; + esac + done + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/cron.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$user" || -z "$state" ]]; then + echo "${tr[missing_args]:-❌ [cron] Faltan argumentos obligatorios: 'user' y 'state'}" + return 1 + fi + + if ! id "$user" &>/dev/null; then + echo "$(render_msg "${tr[user_not_found]}" "user=$user")" + return 1 + fi + + local tag="# shflow:$alias" + local tmpfile + tmpfile=$(mktemp) + + echo "$(render_msg "${tr[checking]}" "user=$user")" + sudo crontab -u "$user" -l 2>/dev/null > "$tmpfile" || true + + case "$state" in + list) + echo "$(render_msg "${tr[list]}" "user=$user")" + grep -E "^.*$tag|^[^#]" "$tmpfile" || echo "${tr[no_entries]:-⚠️ [cron] No hay entradas visibles}" + rm -f "$tmpfile" + return 0 + ;; + absent) + if grep -q "$tag" "$tmpfile"; then + grep -v "$tag" "$tmpfile" > "${tmpfile}.new" + sudo crontab -u "$user" "${tmpfile}.new" + echo "$(render_msg "${tr[removed]}" "alias=$alias")" + rm -f "${tmpfile}.new" + else + echo "$(render_msg "${tr[not_found]}" "alias=$alias")" + fi + rm -f "$tmpfile" + return 0 + ;; + present) + if [[ -z "$alias" || -z "$schedule" || -z "$command" ]]; then + echo "${tr[missing_present]:-❌ [cron] Para 'present' se requieren: alias, schedule y command}" + rm -f "$tmpfile" + return 1 + fi + grep -v "$tag" "$tmpfile" > "${tmpfile}.new" + echo "$schedule $command $tag" >> "${tmpfile}.new" + sudo crontab -u "$user" "${tmpfile}.new" + echo "$(render_msg "${tr[added]}" "alias=$alias")" + rm -f "$tmpfile" "${tmpfile}.new" + return 0 + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "state=$state")" + rm -f "$tmpfile" + return 1 + ;; + esac +} + +check_dependencies_cron() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/cron.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v sudo &>/dev/null; then + echo "${tr[missing_sudo]:-❌ [cron] El comando 'sudo' no está disponible}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [cron] Dependencias OK}" + return 0 +} diff --git a/core/modules/cron.tr.en b/core/modules/cron.tr.en new file mode 100644 index 0000000..404c5f0 --- /dev/null +++ b/core/modules/cron.tr.en @@ -0,0 +1,12 @@ +missing_args=❌ [cron] Missing required arguments: 'user' and 'state' +user_not_found=❌ [cron] User '{user}' does not exist on the system +checking=🕒 [cron] Checking cron entries for user '{user}'... +list=📋 [cron] Current entries for '{user}': +no_entries=⚠️ [cron] No visible entries +removed=➖ [cron] Entry '{alias}' successfully removed +not_found=⚠️ [cron] Entry '{alias}' not found, nothing removed +missing_present=❌ [cron] For 'present', alias, schedule and command are required +added=➕ [cron] Entry '{alias}' successfully created/modified +unsupported=❌ [cron] Unknown state: '{state}'. Use 'present', 'absent' or 'list' +missing_sudo=❌ [cron] 'sudo' command is not available +deps_ok=✅ [cron] Dependencies OK diff --git a/core/modules/cron.tr.es b/core/modules/cron.tr.es new file mode 100644 index 0000000..683e2f8 --- /dev/null +++ b/core/modules/cron.tr.es @@ -0,0 +1,12 @@ +missing_args=❌ [cron] Faltan argumentos obligatorios: 'user' y 'state' +user_not_found=❌ [cron] El usuario '{user}' no existe en el sistema +checking=🕒 [cron] Revisando entradas de cron para usuario '{user}'... +list=📋 [cron] Entradas actuales para '{user}': +no_entries=⚠️ [cron] No hay entradas visibles +removed=➖ [cron] Entrada '{alias}' eliminada correctamente +not_found=⚠️ [cron] Entrada '{alias}' no encontrada, no se eliminó nada +missing_present=❌ [cron] Para 'present' se requieren: alias, schedule y command +added=➕ [cron] Entrada '{alias}' creada/modificada correctamente +unsupported=❌ [cron] Estado desconocido: '{state}'. Usa 'present', 'absent' o 'list' +missing_sudo=❌ [cron] El comando 'sudo' no está disponible +deps_ok=✅ [cron] Dependencias OK diff --git a/core/modules/docker.sh b/core/modules/docker.sh new file mode 100644 index 0000000..9d3350e --- /dev/null +++ b/core/modules/docker.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# License: GPLv3 +# Module: docker +# Description: Gestiona contenedores Docker (run, stop, remove, build, exec) +# Author: Luis GuLo +# Version: 1.7.0 +# Dependencies: ssh, docker + +docker_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local action="${args[action]}" + local become="${args[become]:-false}" + local detach="${args[detach]:-true}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + local detached="-d" + [ "$detach" = "false" ] && detached="" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/docker.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + local name="" image="" path="" command="" + case "$action" in present|stopped|absent|exec) name="${args[name]}" ;; esac + case "$action" in present|build) image="${args[image]}" ;; esac + [[ "$action" == "build" ]] && path="${args[path]}" + [[ "$action" == "exec" ]] && command="${args[command]}" + + case "$action" in + present) + local extra="${args[run_args]:-${args[extra_args]:-}}" + echo "$(render_msg "${tr[run]}" "name=$name" "image=$image")" + ssh "$host" "$prefix docker ps -a --format '{{.Names}}' | grep -q '^$name$' || $prefix docker run $detached --name '$name' $extra '$image'" + ;; + stopped) + echo "$(render_msg "${tr[stop]}" "name=$name")" + ssh "$host" "$prefix docker ps --format '{{.Names}}' | grep -q '^$name$' && $prefix docker stop '$name'" + ;; + absent) + echo "$(render_msg "${tr[remove]}" "name=$name")" + ssh "$host" "$prefix docker ps -a --format '{{.Names}}' | grep -q '^$name$' && $prefix docker rm -f '$name'" + ;; + build) + echo "$(render_msg "${tr[build]}" "image=$image" "path=$path")" + ssh "$host" "cd '$path' && $prefix docker build -t '$image' ." + ;; + exec) + echo "$(render_msg "${tr[exec]}" "name=$name" "command=$command")" + ssh "$host" "$prefix docker exec '$name' $command" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +check_dependencies_docker() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/docker.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_ssh]:-❌ [docker] ssh no está disponible.}" + return 1 + fi + echo "${tr[ssh_ok]:-✅ [docker] ssh disponible.}" + + if ! command -v docker &> /dev/null; then + echo "${tr[missing_docker]:-⚠️ [docker] docker no disponible localmente. Se asumirá que existe en el host remoto.}" + else + echo "${tr[docker_ok]:-✅ [docker] docker disponible localmente.}" + fi + return 0 +} diff --git a/core/modules/docker.tr.en b/core/modules/docker.tr.en new file mode 100644 index 0000000..59844f2 --- /dev/null +++ b/core/modules/docker.tr.en @@ -0,0 +1,10 @@ +run=🧪 [docker] Running container '{name}' with image '{image}' +stop=🛑 [docker] Stopping container '{name}' +remove=🧹 [docker] Removing container '{name}' +build=🏗️ [docker] Building image '{image}' from '{path}' +exec=🚀 [docker] Executing command in '{name}': {command} +unsupported=❌ [docker] Unsupported action '{action}' +missing_ssh=❌ [docker] ssh is not available. +ssh_ok=✅ [docker] ssh is available. +missing_docker=⚠️ [docker] docker not available locally. Assuming it exists on the remote host. +docker_ok=✅ [docker] docker available locally. diff --git a/core/modules/docker.tr.es b/core/modules/docker.tr.es new file mode 100644 index 0000000..4abe373 --- /dev/null +++ b/core/modules/docker.tr.es @@ -0,0 +1,10 @@ +run=🧪 [docker] Ejecutando contenedor '{name}' con imagen '{image}' +stop=🛑 [docker] Deteniendo contenedor '{name}' +remove=🧹 [docker] Eliminando contenedor '{name}' +build=🏗️ [docker] Construyendo imagen '{image}' desde '{path}' +exec=🚀 [docker] Ejecutando comando en '{name}': {command} +unsupported=❌ [docker] Acción '{action}' no soportada +missing_ssh=❌ [docker] ssh no está disponible. +ssh_ok=✅ [docker] ssh disponible. +missing_docker=⚠️ [docker] docker no disponible localmente. Se asumirá que existe en el host remoto. +docker_ok=✅ [docker] docker disponible localmente. diff --git a/core/modules/download.sh b/core/modules/download.sh new file mode 100644 index 0000000..a71fcc4 --- /dev/null +++ b/core/modules/download.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Module: download +# Description: Descarga ficheros remotos con soporte para reintentos, proxy y reanudación +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: wget o curl, sudo (si become=true) + +download_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local url="${args[url]}" + local dest="${args[dest]:-$(basename "$url")}" + local proxy="${args[proxy]:-}" + local continue="${args[continue]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/download.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if [[ -z "$url" ]]; then + echo "${tr[missing_url]:-❌ [download] Falta el parámetro obligatorio 'url'}" + return 1 + fi + + local cmd="" + if command -v wget &>/dev/null; then + echo "${tr[using_wget]:-📦 [download] Usando wget}" + cmd="$prefix wget \"$url\" -O \"$dest\"" + [[ "$continue" == "true" ]] && cmd="$cmd -c" + [[ -n "$proxy" ]] && cmd="$cmd -e use_proxy=yes -e http_proxy=\"$proxy\"" + elif command -v curl &>/dev/null; then + echo "${tr[using_curl]:-📦 [download] Usando curl}" + cmd="$prefix curl -L \"$url\" -o \"$dest\"" + [[ "$continue" == "true" ]] && cmd="$cmd -C -" + [[ -n "$proxy" ]] && cmd="$cmd --proxy \"$proxy\"" + else + echo "${tr[missing_tool]:-❌ [download] Ni wget ni curl están disponibles}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "url=$url" "dest=$dest")" + eval "$cmd" && echo "$(render_msg "${tr[done]}" "dest=$dest")" +} + +check_dependencies_download() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/download.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + if ! command -v wget &>/dev/null && ! command -v curl &>/dev/null; then + echo "${tr[missing_tool]:-❌ [download] Se requiere 'wget' o 'curl'}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [download] Herramienta de descarga disponible}" + return 0 +} diff --git a/core/modules/download.tr.en b/core/modules/download.tr.en new file mode 100644 index 0000000..04526c4 --- /dev/null +++ b/core/modules/download.tr.en @@ -0,0 +1,7 @@ +missing_url=❌ [download] Missing required parameter 'url' +using_wget=📦 [download] Using wget +using_curl=📦 [download] Using curl +missing_tool=❌ [download] Neither wget nor curl are available +start=🔧 [download] Downloading '{url}' → '{dest}' +done=✅ [download] Download completed: {dest} +deps_ok=✅ [download] Download tool available diff --git a/core/modules/download.tr.es b/core/modules/download.tr.es new file mode 100644 index 0000000..d4f54d3 --- /dev/null +++ b/core/modules/download.tr.es @@ -0,0 +1,7 @@ +missing_url=❌ [download] Falta el parámetro obligatorio 'url' +using_wget=📦 [download] Usando wget +using_curl=📦 [download] Usando curl +missing_tool=❌ [download] Ni wget ni curl están disponibles +start=🔧 [download] Descargando '{url}' → '{dest}' +done=✅ [download] Descarga completada: {dest} +deps_ok=✅ [download] Herramienta de descarga disponible diff --git a/core/modules/echo.sh b/core/modules/echo.sh new file mode 100644 index 0000000..8b562a5 --- /dev/null +++ b/core/modules/echo.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Module: echo +# Description: Muestra un mensaje en consola con soporte para variables ShFlow +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: - + +echo_task() { + local host="$1"; shift + declare -A args + + while [[ "$#" -gt 0 ]]; do + case "$1" in + *=*) + key="${1%%=*}" + value="${1#*=}" + args["$key"]="$value" + ;; + esac + shift + done + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/echo.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # 🔁 Interpolar usando argumentos explícitos + for key in "${!args[@]}"; do + for var in "${!args[@]}"; do + args["$key"]="${args[$key]//\{\{ $var \}\}/${args[$var]}}" + done + done + + local message="${args[message]}" + echo "$(render_msg "${tr[output]}" "message=$message")" +} + +check_dependencies_echo() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/echo.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + echo "${tr[deps_ok]:-✅ [echo] No requiere dependencias.}" + return 0 +} diff --git a/core/modules/echo.tr.en b/core/modules/echo.tr.en new file mode 100644 index 0000000..ab2154a --- /dev/null +++ b/core/modules/echo.tr.en @@ -0,0 +1,2 @@ +output=📣 [echo] {message} +deps_ok=✅ [echo] No dependencies required. diff --git a/core/modules/echo.tr.es b/core/modules/echo.tr.es new file mode 100644 index 0000000..ae12ef8 --- /dev/null +++ b/core/modules/echo.tr.es @@ -0,0 +1,2 @@ +output=📣 [echo] {message} +deps_ok=✅ [echo] No requiere dependencias. diff --git a/core/modules/facts.sh b/core/modules/facts.sh new file mode 100644 index 0000000..498ccd0 --- /dev/null +++ b/core/modules/facts.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Module: facts +# Description: Extrae información del sistema con opciones de formato, filtrado y salida +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.4.0 +# Dependencies: lscpu, ip, free, lsblk, uname, hostnamectl + +facts_task() { + local host="$1"; shift + declare -A args + local field="" format="plain" output="" append="false" host_label="" + + for arg in "$@"; do + key="${arg%%=*}"; value="${arg#*=}" + case "$key" in + field) field="$value" ;; + format) format="${value,,}" ;; + output) output="$value" ;; + append) append="$value" ;; + host_label) host_label="$value" ;; + esac + done + + [[ -z "$host_label" ]] && host_label="$host" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/facts.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local prefix="" + [[ "$host" != "localhost" ]] && prefix="ssh $host" + + [[ "$DEBUG" == "true" ]] && echo "$(render_msg "${tr[debug_prefix]}" "prefix=$prefix")" + + local raw + raw=$($prefix bash --noprofile --norc <<'EOF' + cd /tmp || cd ~ + echo "hostname=$(hostname)" + lscpu | awk '/^CPU\(s\):/ {print "cpu_count="$2}' + free -m | awk '/Mem:/ {print "ram_total_mb="$2}' + if command -v hostnamectl &> /dev/null; then + hostnamectl | awk -F: '/Operating System/ {print "os_name=" $2}' | sed 's/^ *//' + hostnamectl | awk -F: '/Kernel/ {print "os_version=" $2}' | sed 's/^ *//' + else + echo "os_name=$(uname -s)" + echo "os_version=$(uname -r)" + fi + ip link show | awk -F: '/^[0-9]+: / {print $2}' | grep -Ev 'docker|virbr|lo|veth|br-' | while read -r dev; do + ip=$(ip -4 addr show "$dev" | awk '/inet / {print $2}' | cut -d/ -f1) + mac=$(ip link show "$dev" | awk '/ether/ {print $2}') + [[ -n "$ip" || -n "$mac" ]] && echo "net_$dev=IP:$ip MAC:$mac" + done + ip -4 addr show | awk '/inet / {print $2}' | cut -d/ -f1 | paste -sd ' ' - | awk '{print "ip_addresses="$0}' + lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT | grep -Ev 'loop|tmpfs|overlay|docker' | awk 'NR>1 && NF>0 {print "partition_list=" $1 " " $2 " " $3 " " $4}' +EOF +) + + [[ -n "$field" ]] && raw=$(echo "$raw" | grep "^$field=") + + local partitions=() facts=() + while IFS= read -r line; do + [[ "$line" == partition_list=* ]] && partitions+=("${line#*=}") || facts+=("$line") + done <<< "$raw" + + local formatted="" + case "$format" in + plain) + formatted+="Host: $host_label\n" + for f in "${facts[@]}"; do formatted+="${f%%=*}: ${f#*=}\n"; done + [[ ${#partitions[@]} -gt 0 ]] && formatted+="partitions:\n" && for p in "${partitions[@]}"; do formatted+=" - $p\n"; done + ;; + md) + formatted+="### $host_label\n" + for f in "${facts[@]}"; do formatted+="- **${f%%=*}:** ${f#*=}\n"; done + [[ ${#partitions[@]} -gt 0 ]] && formatted+="- **partitions:**\n" && for p in "${partitions[@]}"; do formatted+=" - $p\n"; done + ;; + kv) + for f in "${facts[@]}"; do formatted+="$f\n"; done + [[ ${#partitions[@]} -gt 0 ]] && formatted+="partitions=$(IFS=';'; echo "${partitions[*]}")\n" + ;; + json) + local json="{" + for f in "${facts[@]}"; do json+="\"${f%%=*}\":\"${f#*=}\","; done + [[ ${#partitions[@]} -gt 0 ]] && json+="\"partitions\":[" && for p in "${partitions[@]}"; do json+="\"$p\","; done && json="${json%,}]" || json="${json%,}" + json+="}" + formatted="$json" + ;; + *) + echo "$(render_msg "${tr[unsupported_format]}" "format=$format")" + return 1 + ;; + esac + + if [[ -n "$output" ]]; then + [[ "$append" == "true" ]] && echo -e "$formatted" >> "$output" || echo -e "$formatted" > "$output" + echo "$(render_msg "${tr[saved]}" "output=$output")" + else + echo -e "$formatted" + fi +} + +check_dependencies_facts() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/facts.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + + for cmd in lscpu ip free lsblk uname hostnamectl; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_cmd]}" "cmd=$cmd")" + else + echo "$(render_msg "${tr[cmd_ok]}" "cmd=$cmd")" + fi + done + return 0 +} diff --git a/core/modules/facts.tr.en b/core/modules/facts.tr.en new file mode 100644 index 0000000..015ef5b --- /dev/null +++ b/core/modules/facts.tr.en @@ -0,0 +1,5 @@ +debug_prefix=🔍 SSH line: {prefix} +unsupported_format=❌ [facts] Format '{format}' not supported. +saved=💾 [facts] Report saved to: {output} +missing_cmd=⚠️ [facts] '{cmd}' not available locally. Assuming it exists on remote host. +cmd_ok=✅ [facts] '{cmd}' available locally. diff --git a/core/modules/facts.tr.es b/core/modules/facts.tr.es new file mode 100644 index 0000000..7d2b08b --- /dev/null +++ b/core/modules/facts.tr.es @@ -0,0 +1,5 @@ +debug_prefix=🔍 Línea SSH: {prefix} +unsupported_format=❌ [facts] Formato '{format}' no soportado. +saved=💾 [facts] Informe guardado en: {output} +missing_cmd=⚠️ [facts] '{cmd}' no disponible localmente. Se asumirá que existe en el host remoto. +cmd_ok=✅ [facts] '{cmd}' disponible localmente. diff --git a/core/modules/file.sh b/core/modules/file.sh new file mode 100644 index 0000000..44804af --- /dev/null +++ b/core/modules/file.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Module: file +# Description: Gestiona archivos y directorios remotos (crear, eliminar, permisos) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: ssh + +file_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local path="${args[path]}" + local state="${args[state]}" + local type="${args[type]}" + local mode="${args[mode]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$state" in + present) + if [[ "$type" == "directory" ]]; then + echo "$(render_msg "${tr[creating_dir]}" "path=$path")" + ssh "$host" "[ -d '$path' ] || $prefix mkdir -p '$path'" + elif [[ "$type" == "file" ]]; then + echo "$(render_msg "${tr[creating_file]}" "path=$path")" + ssh "$host" "[ -f '$path' ] || $prefix touch '$path'" + fi + if [[ -n "$mode" ]]; then + echo "$(render_msg "${tr[setting_mode]}" "mode=$mode" "path=$path")" + ssh "$host" "$prefix chmod $mode '$path'" + fi + ;; + absent) + if [[ "$type" == "directory" ]]; then + echo "$(render_msg "${tr[removing_dir]}" "path=$path")" + ssh "$host" "[ -d '$path' ] && $prefix rm -rf '$path'" + elif [[ "$type" == "file" ]]; then + echo "$(render_msg "${tr[removing_file]}" "path=$path")" + ssh "$host" "[ -f '$path' ] && $prefix rm -f '$path'" + fi + ;; + *) + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_file() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [file] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [file] ssh disponible.}" + return 0 +} diff --git a/core/modules/file.tr.en b/core/modules/file.tr.en new file mode 100644 index 0000000..2d11c91 --- /dev/null +++ b/core/modules/file.tr.en @@ -0,0 +1,8 @@ +creating_dir=📁 [file] Creating directory: {path} +creating_file=📄 [file] Creating file: {path} +setting_mode=🔐 [file] Setting permissions {mode} on {path} +removing_dir=🧹 [file] Removing directory: {path} +removing_file=🧹 [file] Removing file: {path} +unsupported_state=❌ [file] Unsupported state '{state}'. Use present or absent. +missing_deps=❌ [file] ssh is not available. +deps_ok=✅ [file] ssh is available. diff --git a/core/modules/file.tr.es b/core/modules/file.tr.es new file mode 100644 index 0000000..4123cad --- /dev/null +++ b/core/modules/file.tr.es @@ -0,0 +1,8 @@ +creating_dir=📁 [file] Creando directorio: {path} +creating_file=📄 [file] Creando archivo: {path} +setting_mode=🔐 [file] Estableciendo permisos {mode} en {path} +removing_dir=🧹 [file] Eliminando directorio: {path} +removing_file=🧹 [file] Eliminando archivo: {path} +unsupported_state=❌ [file] Estado '{state}' no soportado. Usa present o absent. +missing_deps=❌ [file] ssh no está disponible. +deps_ok=✅ [file] ssh disponible. diff --git a/core/modules/file_read.sh b/core/modules/file_read.sh new file mode 100644 index 0000000..e21423f --- /dev/null +++ b/core/modules/file_read.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Module: file_read +# Description: Lee el contenido de un archivo remoto, con opción de filtrado por patrón +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: ssh, cat, grep + +file_read_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local path="${args[path]}" + local grep="${args[grep]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file_read.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ -z "$path" ]]; then + echo "${tr[missing_path]:-❌ [file_read] Parámetro 'path' obligatorio}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "path=$path" "host=$host")" + + if [[ -n "$grep" ]]; then + ssh "$host" "$prefix grep -E '$grep' '$path'" + else + ssh "$host" "$prefix cat '$path'" + fi +} + +check_dependencies_file_read() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/file_read.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null || ! command -v grep &> /dev/null; then + echo "${tr[missing_deps]:-❌ [file_read] ssh o grep no están disponibles.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [file_read] ssh y grep disponibles.}" + return 0 +} diff --git a/core/modules/file_read.tr.en b/core/modules/file_read.tr.en new file mode 100644 index 0000000..117764e --- /dev/null +++ b/core/modules/file_read.tr.en @@ -0,0 +1,4 @@ +missing_path=❌ [file_read] Parameter 'path' is required +start=📄 [file_read] Reading file '{path}' on {host}... +missing_deps=❌ [file_read] ssh or grep are not available. +deps_ok=✅ [file_read] ssh and grep are available. diff --git a/core/modules/file_read.tr.es b/core/modules/file_read.tr.es new file mode 100644 index 0000000..0331aa7 --- /dev/null +++ b/core/modules/file_read.tr.es @@ -0,0 +1,4 @@ +missing_path=❌ [file_read] Parámetro 'path' obligatorio +start=📄 [file_read] Leyendo archivo '{path}' en {host}... +missing_deps=❌ [file_read] ssh o grep no están disponibles. +deps_ok=✅ [file_read] ssh y grep disponibles. diff --git a/core/modules/fs.sh b/core/modules/fs.sh new file mode 100644 index 0000000..1b16bad --- /dev/null +++ b/core/modules/fs.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Module: fs +# Description: Operaciones remotas sobre ficheros (mover, renombrar, copiar, borrar, truncar) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.3.0 +# Dependencies: ssh + +fs_task() { + local host="$1"; shift + declare -A args + local files=() + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/fs.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # Parseo de argumentos + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + if [[ "$key" == "files" ]]; then + if [[ "$value" == *'*'* || "$value" == *'?'* || "$value" == *'['* ]]; then + mapfile -t files < <(ssh "$host" "ls -1 $value 2>/dev/null") + else + IFS=',' read -r -a files <<< "$value" + fi + else + args["$key"]="$value" + fi + done + + local action="${args[action]}" + local src="${args[src]}" + local dest="${args[dest]}" + local path="${args[path]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + case "$action" in + move|rename|copy) + if [[ ${#files[@]} -gt 0 ]]; then + for file in "${files[@]}"; do + base="$(basename "$file")" + target="$dest/$base" + cmd="$prefix mv"; [[ "$action" == "copy" ]] && cmd="$prefix cp" + ssh "$host" "$cmd '$file' '$target'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$file" "dest=$target")" + done + else + cmd="$prefix mv"; [[ "$action" == "copy" ]] && cmd="$prefix cp" + ssh "$host" "$cmd '$src' '$dest'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$src" "dest=$dest")" + fi + ;; + delete|truncate) + if [[ ${#files[@]} -gt 0 ]]; then + for file in "${files[@]}"; do + cmd="$prefix rm -f"; [[ "$action" == "truncate" ]] && cmd="$prefix truncate -s 0" + ssh "$host" "$cmd '$file'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$file")" + done + else + cmd="$prefix rm -f"; [[ "$action" == "truncate" ]] && cmd="$prefix truncate -s 0" + ssh "$host" "$cmd '$path'" && echo "$(render_msg "${tr[action_ok]}" "action=$action" "src=$path")" + fi + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +check_dependencies_fs() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/fs.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [fs] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [fs] ssh disponible.}" + return 0 +} diff --git a/core/modules/fs.tr.en b/core/modules/fs.tr.en new file mode 100644 index 0000000..4e0b2ef --- /dev/null +++ b/core/modules/fs.tr.en @@ -0,0 +1,4 @@ +action_ok=📁 [{action}] {src} → {dest} +unsupported=❌ [fs] Unsupported action '{action}'. +missing_deps=❌ [fs] ssh is not available. +deps_ok=✅ [fs] ssh is available. diff --git a/core/modules/fs.tr.es b/core/modules/fs.tr.es new file mode 100644 index 0000000..35f3e0a --- /dev/null +++ b/core/modules/fs.tr.es @@ -0,0 +1,4 @@ +action_ok=📁 [{action}] {src} → {dest} +unsupported=❌ [fs] Acción '{action}' no soportada. +missing_deps=❌ [fs] ssh no está disponible. +deps_ok=✅ [fs] ssh disponible. diff --git a/core/modules/git.sh b/core/modules/git.sh new file mode 100644 index 0000000..0fa22f1 --- /dev/null +++ b/core/modules/git.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Module: git +# Description: Gestiona repositorios Git en hosts remotos (clone, pull, checkout, fetch-file) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: ssh, git, curl, tar + +git_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local action="${args[action]}" + local repo="${args[repo]}" + local dest="${args[dest]}" + local branch="${args[branch]}" + local file_path="${args[file_path]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/git.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$action" in + clone) + echo "$(render_msg "${tr[cloning]}" "repo=$repo" "dest=$dest")" + ssh "$host" "[ -d '$dest/.git' ] || $prefix git clone '$repo' '$dest'" + ;; + pull) + echo "$(render_msg "${tr[pulling]}" "dest=$dest")" + ssh "$host" "[ -d '$dest/.git' ] && cd '$dest' && $prefix git pull" + ;; + checkout) + echo "$(render_msg "${tr[checkout]}" "branch=$branch" "dest=$dest")" + ssh "$host" "[ -d '$dest/.git' ] && cd '$dest' && $prefix git checkout '$branch'" + ;; + fetch-file) + echo "$(render_msg "${tr[fetching]}" "file=$file_path" "repo=$repo" "branch=$branch")" + fetch_file_from_repo "$host" "$repo" "$branch" "$file_path" "$dest" "$become" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +fetch_file_from_repo() { + local host="$1" + local repo="$2" + local branch="$3" + local file_path="$4" + local dest="$5" + local become="$6" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + ssh "$host" "$prefix git archive --remote='$repo' '$branch' '$file_path' | $prefix tar -xO > '$dest'" +} + +check_dependencies_git() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/git.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in ssh git curl tar; do + command -v "$cmd" &> /dev/null || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [git] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/git.tr.en b/core/modules/git.tr.en new file mode 100644 index 0000000..ca27acd --- /dev/null +++ b/core/modules/git.tr.en @@ -0,0 +1,7 @@ +cloning=📥 [git] Cloning repository '{repo}' into '{dest}' +pulling=🔄 [git] Running git pull in '{dest}' +checkout=📦 [git] Switching to branch '{branch}' in '{dest}' +fetching=📄 [git] Fetching file '{file}' from '{repo}' branch '{branch}' +unsupported=❌ [git] Unsupported action '{action}' +missing_deps=❌ [git] Missing dependencies: {cmds} +deps_ok=✅ [git] All dependencies are available diff --git a/core/modules/git.tr.es b/core/modules/git.tr.es new file mode 100644 index 0000000..60a68f7 --- /dev/null +++ b/core/modules/git.tr.es @@ -0,0 +1,7 @@ +cloning=📥 [git] Clonando repositorio '{repo}' en '{dest}' +pulling=🔄 [git] Ejecutando git pull en '{dest}' +checkout=📦 [git] Cambiando a rama '{branch}' en '{dest}' +fetching=📄 [git] Extrayendo archivo '{file}' desde '{repo}' rama '{branch}' +unsupported=❌ [git] Acción '{action}' no soportada +missing_deps=❌ [git] Dependencias faltantes: {cmds} +deps_ok=✅ [git] Todas las dependencias están disponibles diff --git a/core/modules/groups.sh b/core/modules/groups.sh new file mode 100644 index 0000000..e446b12 --- /dev/null +++ b/core/modules/groups.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Module: groups +# Description: Gestiona grupos del sistema (crear, modificar, eliminar) +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: getent, groupadd, groupmod, groupdel, sudo + +groups_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local groupname="${args[groupname]}" + local gid="${args[gid]:-}" + local state="${args[state]:-create}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/groups.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # 🛡️ Validación + if [[ "$become" != "true" && "$EUID" -ne 0 ]]; then + echo "${tr[priv_required]:-❌ [groups] Se requieren privilegios para gestionar grupos. Usa 'become: true'.}" + return 1 + fi + + if [[ -z "$groupname" ]]; then + echo "${tr[missing_groupname]:-❌ [groups] Falta el parámetro obligatorio 'groupname'}" + return 1 + fi + + case "$state" in + create) + echo "${tr[enter_create]:-🔧 [groups] Entrando en create}" + if getent group "$groupname" &>/dev/null; then + echo "$(render_msg "${tr[exists]}" "groupname=$groupname")" + return 0 + fi + local cmd="$prefix groupadd \"$groupname\"" + [[ -n "$gid" ]] && cmd="$cmd -g \"$gid\"" + eval "$cmd" && echo "$(render_msg "${tr[created]}" "groupname=$groupname")" + ;; + modify) + echo "${tr[enter_modify]:-🔧 [groups] Entrando en modify}" + if ! getent group "$groupname" &>/dev/null; then + echo "$(render_msg "${tr[not_exists]}" "groupname=$groupname")" + return 1 + fi + [[ -z "$gid" ]] && echo "${tr[nothing_to_modify]:-⚠️ [groups] Nada que modificar: falta 'gid'}" && return 0 + eval "$prefix groupmod -g \"$gid\" \"$groupname\"" && echo "$(render_msg "${tr[modified]}" "groupname=$groupname")" + ;; + absent) + echo "${tr[enter_absent]:-🔧 [groups] Entrando en absent}" + if ! getent group "$groupname" &>/dev/null; then + echo "$(render_msg "${tr[already_deleted]}" "groupname=$groupname")" + return 0 + fi + eval "$prefix groupdel \"$groupname\"" && echo "$(render_msg "${tr[deleted]}" "groupname=$groupname")" + ;; + *) + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_groups() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/groups.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in getent sudo; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [groups] Todas las dependencias están presentes}" + return 0 +} diff --git a/core/modules/groups.tr.en b/core/modules/groups.tr.en new file mode 100644 index 0000000..6df77e1 --- /dev/null +++ b/core/modules/groups.tr.en @@ -0,0 +1,15 @@ +priv_required=❌ [groups] Privileges required to manage groups. Use 'become: true'. +missing_groupname=❌ [groups] Missing required parameter 'groupname' +enter_create=🔧 [groups] Entering create +enter_modify=🔧 [groups] Entering modify +enter_absent=🔧 [groups] Entering absent +exists=✅ [groups] Group '{groupname}' already exists +not_exists=⚠️ [groups] Group '{groupname}' does not exist +nothing_to_modify=⚠️ [groups] Nothing to modify: missing 'gid' +created=✅ [groups] Group '{groupname}' created +modified=✅ [groups] Group '{groupname}' modified +deleted=✅ [groups] Group '{groupname}' deleted +already_deleted=✅ [groups] Group '{groupname}' already deleted +unsupported_state=❌ [groups] Unsupported state '{state}'. Use create, modify or absent. +missing_deps=❌ [groups] Missing dependencies: {cmds} +deps_ok=✅ [groups] All dependencies are present diff --git a/core/modules/groups.tr.es b/core/modules/groups.tr.es new file mode 100644 index 0000000..70e1492 --- /dev/null +++ b/core/modules/groups.tr.es @@ -0,0 +1,15 @@ +priv_required=❌ [groups] Se requieren privilegios para gestionar grupos. Usa 'become: true'. +missing_groupname=❌ [groups] Falta el parámetro obligatorio 'groupname' +enter_create=🔧 [groups] Entrando en create +enter_modify=🔧 [groups] Entrando en modify +enter_absent=🔧 [groups] Entrando en absent +exists=✅ [groups] Grupo '{groupname}' ya existe +not_exists=⚠️ [groups] Grupo '{groupname}' no existe +nothing_to_modify=⚠️ [groups] Nada que modificar: falta 'gid' +created=✅ [groups] Grupo '{groupname}' creado +modified=✅ [groups] Grupo '{groupname}' modificado +deleted=✅ [groups] Grupo '{groupname}' eliminado +already_deleted=✅ [groups] Grupo '{groupname}' ya eliminado +unsupported_state=❌ [groups] Estado '{state}' no soportado. Usa create, modify o absent. +missing_deps=❌ [groups] Dependencias faltantes: {cmds} +deps_ok=✅ [groups] Todas las dependencias están presentes diff --git a/core/modules/lineinfile.sh b/core/modules/lineinfile.sh new file mode 100644 index 0000000..aded42c --- /dev/null +++ b/core/modules/lineinfile.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Module: lineinfile +# Description: Asegura la presencia o reemplazo de una línea en un archivo +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: grep, sed, tee, awk + +lineinfile_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local path="${args[path]}" + local line="${args[line]}" + local regexp="${args[regexp]}" + local insert_after="${args[insert_after]}" + local create="${args[create]:-true}" + local backup="${args[backup]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lineinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ ! -f "$path" ]]; then + if [[ "$create" == "true" ]]; then + echo "$(render_msg "${tr[creating]}" "path=$path")" + touch "$path" + else + echo "$(render_msg "${tr[missing_file]}" "path=$path")" + return 1 + fi + fi + + if [[ "$backup" == "true" ]]; then + cp "$path" "$path.bak" + echo "$(render_msg "${tr[backup]}" "path=$path")" + fi + + if [[ -n "$regexp" && $(grep -Eq "$regexp" "$path" && echo "yes") == "yes" ]]; then + echo "$(render_msg "${tr[replacing]}" "regexp=$regexp")" + $prefix sed -i "s|$regexp|$line|" "$path" + return 0 + fi + + if [[ -n "$insert_after" && $(grep -q "$insert_after" "$path" && echo "yes") == "yes" ]]; then + echo "$(render_msg "${tr[inserting]}" "after=$insert_after")" + $prefix sed -i "/$insert_after/a $line" "$path" + return 0 + fi + + if grep -Fxq "$line" "$path"; then + echo "$(render_msg "${tr[exists]}" "line=$line")" + return 0 + fi + + echo "${tr[appending]:-➕ [lineinfile] Añadiendo línea al final}" + echo "$line" | $prefix tee -a "$path" > /dev/null +} + +check_dependencies_lineinfile() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lineinfile.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in grep sed tee awk; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [lineinfile] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/lineinfile.tr.en b/core/modules/lineinfile.tr.en new file mode 100644 index 0000000..15054eb --- /dev/null +++ b/core/modules/lineinfile.tr.en @@ -0,0 +1,9 @@ +creating=📄 [lineinfile] Creating file: {path} +missing_file=❌ [lineinfile] File does not exist and create=false +backup=📦 Backup created: {path}.bak +replacing=🔁 [lineinfile] Replacing line matching: {regexp} +inserting=➕ [lineinfile] Inserting after: {after} +exists=✅ [lineinfile] Line already present: "{line}" +appending=➕ [lineinfile] Appending line at the end +missing_deps=❌ [lineinfile] Missing dependencies: {cmds} +deps_ok=✅ [lineinfile] All dependencies are available diff --git a/core/modules/lineinfile.tr.es b/core/modules/lineinfile.tr.es new file mode 100644 index 0000000..c28d6ca --- /dev/null +++ b/core/modules/lineinfile.tr.es @@ -0,0 +1,9 @@ +creating=📄 [lineinfile] Creando archivo: {path} +missing_file=❌ [lineinfile] El archivo no existe y create=false +backup=📦 Copia de seguridad creada: {path}.bak +replacing=🔁 [lineinfile] Reemplazando línea que coincide con: {regexp} +inserting=➕ [lineinfile] Insertando después de: {after} +exists=✅ [lineinfile] Línea ya presente: "{line}" +appending=➕ [lineinfile] Añadiendo línea al final +missing_deps=❌ [lineinfile] Dependencias faltantes: {cmds} +deps_ok=✅ [lineinfile] Todas las dependencias están disponibles diff --git a/core/modules/lookup.sh b/core/modules/lookup.sh new file mode 100644 index 0000000..d3c93bb --- /dev/null +++ b/core/modules/lookup.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Module: lookup +# Description: Recupera secretos cifrados del vault local +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: gpg + +lookup_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local vault_key="${args[key]}" + local vault_dir="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/core/vault" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lookup.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ -f "$vault_dir/$vault_key.gpg" ]]; then + gpg --quiet --batch --yes --passphrase-file "$HOME/.shflow.key" -d "$vault_dir/$vault_key.gpg" 2>/dev/null || \ + gpg --quiet --batch --yes -d "$vault_dir/$vault_key.gpg" + else + echo "$(render_msg "${tr[not_found]}" "key=$vault_key" "path=$vault_dir")" + return 1 + fi +} + +check_dependencies_lookup() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/lookup.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v gpg &> /dev/null; then + echo "${tr[missing_deps]:-❌ [lookup] gpg no disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [lookup] gpg disponible.}" + return 0 +} diff --git a/core/modules/lookup.tr.en b/core/modules/lookup.tr.en new file mode 100644 index 0000000..c20d4b0 --- /dev/null +++ b/core/modules/lookup.tr.en @@ -0,0 +1,3 @@ +not_found=❌ [lookup] Secret '{key}' not found in {path} +missing_deps=❌ [lookup] gpg is not available. +deps_ok=✅ [lookup] gpg is available. diff --git a/core/modules/lookup.tr.es b/core/modules/lookup.tr.es new file mode 100644 index 0000000..e1a89b7 --- /dev/null +++ b/core/modules/lookup.tr.es @@ -0,0 +1,3 @@ +not_found=❌ [lookup] Secreto '{key}' no encontrado en {path} +missing_deps=❌ [lookup] gpg no disponible. +deps_ok=✅ [lookup] gpg disponible. diff --git a/core/modules/loop.sh b/core/modules/loop.sh new file mode 100644 index 0000000..3f7cb0d --- /dev/null +++ b/core/modules/loop.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Module: loop +# Description: Ejecuta un módulo sobre una lista o matriz de valores +# Author: Luis GuLo +# Version: 0.3.0 +# Dependencies: echo, tee + +loop_task() { + local host="$1"; shift + declare -A args + local items_raw="" secondary_raw="" target_module="" + local fail_fast="true" + declare -A module_args + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/loop.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # Parsear argumentos + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + case "$key" in + items) items_raw="$value" ;; + secondary) secondary_raw="$value" ;; + module) target_module="$value" ;; + fail_fast) fail_fast="$value" ;; + *) module_args["$key"]="$value" ;; + esac + done + + if [[ -z "$items_raw" || -z "$target_module" ]]; then + echo "${tr[missing_args]:-❌ [loop] Faltan argumentos obligatorios: items=... module=...}" + return 1 + fi + + IFS=',' read -r -a items <<< "$items_raw" + IFS=',' read -r -a secondary <<< "$secondary_raw" + + for item in "${items[@]}"; do + if [[ "$item" == *:* ]]; then + item_key="${item%%:*}" + item_value="${item#*:}" + else + item_key="$item" + item_value="" + fi + + if [[ -n "$secondary_raw" ]]; then + for sec in "${secondary[@]}"; do + run_module "$host" "$target_module" "$item" "$item_key" "$item_value" "$sec" module_args || { + echo "$(render_msg "${tr[fail_secondary]}" "item=$item" "secondary=$sec")" + [[ "$fail_fast" == "true" ]] && return 1 + } + done + else + run_module "$host" "$target_module" "$item" "$item_key" "$item_value" "" module_args || { + echo "$(render_msg "${tr[fail_item]}" "item=$item")" + [[ "$fail_fast" == "true" ]] && return 1 + } + fi + done +} + +run_module() { + local host="$1" + local module="$2" + local item="$3" + local item_key="$4" + local item_value="$5" + local secondary_item="$6" + declare -n args_ref="$7" + + local call_args=() + for key in "${!args_ref[@]}"; do + value="${args_ref[$key]}" + value="${value//\{\{item\}\}/$item}" + value="${value//\{\{item_key\}\}/$item_key}" + value="${value//\{\{item_value\}\}/$item_value}" + value="${value//\{\{secondary_item\}\}/$secondary_item}" + call_args+=("$key=$value") + done + + echo "🔁 [loop] → $module con item='$item' secondary='$secondary_item'" + + local PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + local MODULE_PATH="" + local SEARCH_PATHS=("$PROJECT_ROOT/core/modules" "$PROJECT_ROOT/user_modules" "$PROJECT_ROOT/community_modules") + for search_dir in "${SEARCH_PATHS[@]}"; do + while IFS= read -r -d '' candidate; do + [[ "$(basename "$candidate")" == "${module}.sh" ]] && MODULE_PATH="$candidate" && break 2 + done < <(find "$search_dir" -type f -name "${module}.sh" -print0) + done + + if [[ -z "$MODULE_PATH" ]]; then + echo "$(render_msg "${tr[module_not_found]}" "module=$module")" + return 1 + fi + + source "$MODULE_PATH" + ! declare -f "${module}_task" > /dev/null && echo "$(render_msg "${tr[task_not_found]}" "module=$module")" && return 1 + + "${module}_task" "$host" "${call_args[@]}" +} + +check_dependencies_loop() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/loop.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in echo tee; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [loop] Dependencias disponibles.}" + return 0 +} diff --git a/core/modules/loop.tr.en b/core/modules/loop.tr.en new file mode 100644 index 0000000..d0de2dd --- /dev/null +++ b/core/modules/loop.tr.en @@ -0,0 +1,7 @@ +missing_args=❌ [loop] Missing required arguments: items=... module=... +fail_item=⚠️ [loop] Iteration failed with '{item}' +fail_secondary=⚠️ [loop] Iteration failed with '{item}' and '{secondary}' +module_not_found=❌ [loop] Module '{module}' not found +task_not_found=❌ [loop] Function '{module}_task' not found +missing_deps=❌ [loop] Missing dependencies: {cmds} +deps_ok=✅ [loop] Dependencies available. diff --git a/core/modules/loop.tr.es b/core/modules/loop.tr.es new file mode 100644 index 0000000..aaf85ec --- /dev/null +++ b/core/modules/loop.tr.es @@ -0,0 +1,7 @@ +missing_args=❌ [loop] Faltan argumentos obligatorios: items=... module=... +fail_item=⚠️ [loop] Falló la iteración con '{item}' +fail_secondary=⚠️ [loop] Falló la iteración con '{item}' y '{secondary}' +module_not_found=❌ [loop] Módulo '{module}' no encontrado +task_not_found=❌ [loop] Función '{module}_task' no encontrada +missing_deps=❌ [loop] Dependencias faltantes: {cmds} +deps_ok=✅ [loop] Dependencias disponibles. diff --git a/core/modules/openssl.sh b/core/modules/openssl.sh new file mode 100644 index 0000000..d872dca --- /dev/null +++ b/core/modules/openssl.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# Module: openssl +# Description: Gestiona certificados y claves con OpenSSL (convertir, inspeccionar, instalar como CA) +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: openssl, sudo, bash + +openssl_task() { + local host="$1"; shift + check_dependencies_openssl || return 1 + + local state="" src="" dest="" format="" password="" alias="" trust_path="" become="false" + for arg in "$@"; do + case "$arg" in + state=*) state="${arg#state=}" ;; + src=*) src="${arg#src=}" ;; + dest=*) dest="${arg#dest=}" ;; + format=*) format="${arg#format=}" ;; + password=*) password="${arg#password=}" ;; + alias=*) alias="${arg#alias=}" ;; + trust_path=*) trust_path="${arg#trust_path=}" ;; + become=*) become="${arg#become=}" ;; + esac + done + + local sudo_cmd="" + [[ "$become" == "true" ]] && sudo_cmd="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/openssl.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$state" in + convert) + if [[ -z "$src" || -z "$dest" || -z "$format" ]]; then + echo "${tr[missing_convert]:-❌ [openssl] Faltan argumentos para conversión: src, dest, format}" + return 1 + fi + if [[ ! -f "$src" ]]; then + echo "$(render_msg "${tr[src_not_found]}" "src=$src")" + return 1 + fi + echo "$(render_msg "${tr[converting]}" "src=$src" "format=$format")" + + case "$format" in + pem) + $sudo_cmd openssl pkcs12 -in "$src" -out "$dest" -nodes -password pass:"$password" && \ + echo "$(render_msg "${tr[converted]}" "dest=$dest")" + ;; + pfx) + $sudo_cmd openssl pkcs12 -export -out "$dest" -inkey "$src" -in "$src" -password pass:"$password" && \ + echo "$(render_msg "${tr[converted]}" "dest=$dest")" + ;; + key) + $sudo_cmd openssl pkey -in "$src" -out "$dest" && \ + echo "$(render_msg "${tr[key_extracted]}" "dest=$dest")" + ;; + cer) + $sudo_cmd openssl x509 -in "$src" -out "$dest" -outform DER && \ + echo "$(render_msg "${tr[cer_converted]}" "dest=$dest")" + ;; + *) + echo "$(render_msg "${tr[unsupported_format]}" "format=$format")" + return 1 + ;; + esac + ;; + + inspect) + if [[ -z "$src" || ! -f "$src" ]]; then + echo "$(render_msg "${tr[missing_inspect]}" "src=$src")" + return 1 + fi + echo "$(render_msg "${tr[inspecting]}" "src=$src")" + $sudo_cmd openssl x509 -in "$src" -noout -text | grep -E 'Subject:|Issuer:|Not Before:|Not After :|Fingerprint' || echo "${tr[inspect_fail]:-⚠️ [openssl] No se pudo extraer información}" + ;; + + trust) + if [[ -z "$src" || -z "$alias" || -z "$trust_path" ]]; then + echo "${tr[missing_trust]:-❌ [openssl] Faltan argumentos para instalación como CA: src, alias, trust_path}" + return 1 + fi + if [[ ! -f "$src" ]]; then + echo "$(render_msg "${tr[src_not_found]}" "src=$src")" + return 1 + fi + echo "$(render_msg "${tr[trusting]}" "alias=$alias")" + $sudo_cmd cp "$src" "$trust_path/$alias.crt" && \ + $sudo_cmd update-ca-certificates && \ + echo "${tr[trusted]:-✅ [openssl] Certificado instalado y CA actualizada}" + ;; + + untrust) + if [[ -z "$alias" || -z "$trust_path" ]]; then + echo "${tr[missing_untrust]:-❌ [openssl] Faltan argumentos para eliminación: alias, trust_path}" + return 1 + fi + local cert_path="$trust_path/$alias.crt" + if [[ ! -f "$cert_path" ]]; then + echo "$(render_msg "${tr[untrust_not_found]}" "alias=$alias" "trust_path=$trust_path")" + return 0 + fi + echo "$(render_msg "${tr[untrusting]}" "alias=$alias")" + $sudo_cmd rm -f "$cert_path" && \ + $sudo_cmd update-ca-certificates && \ + echo "${tr[untrusted]:-✅ [openssl] Certificado eliminado y CA actualizada}" + ;; + + *) + echo "$(render_msg "${tr[unknown_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_openssl() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/openssl.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in openssl sudo; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [openssl] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/openssl.tr.en b/core/modules/openssl.tr.en new file mode 100644 index 0000000..a0caffb --- /dev/null +++ b/core/modules/openssl.tr.en @@ -0,0 +1,20 @@ +missing_convert=❌ [openssl] Missing arguments for conversion: src, dest, format +missing_inspect=❌ [openssl] File not found for inspection: {src} +missing_trust=❌ [openssl] Missing arguments for CA installation: src, alias, trust_path +missing_untrust=❌ [openssl] Missing arguments for removal: alias, trust_path +src_not_found=❌ [openssl] Input file not found: {src} +converting=🔐 [openssl] Converting {src} to format {format}... +converted=✅ [openssl] Conversion completed: {dest} +key_extracted=✅ [openssl] Key extracted: {dest} +cer_converted=✅ [openssl] Certificate converted to .cer: {dest} +unsupported_format=❌ [openssl] Unsupported destination format: {format} +inspecting=🔍 [openssl] Inspecting certificate: {src} +inspect_fail=⚠️ [openssl] Failed to extract certificate information +trusting=🏛️ [openssl] Installing certificate '{alias}' as trusted CA... +trusted=✅ [openssl] Certificate installed and CA updated +untrust_not_found=⚠️ [openssl] Certificate '{alias}' not found in {trust_path} +untrusting=🧹 [openssl] Removing certificate '{alias}' from trusted CAs... +untrusted=✅ [openssl] Certificate removed and CA updated +unknown_state=❌ [openssl] Unknown state: '{state}'. Use 'convert', 'inspect', 'trust' or 'untrust' +missing_deps=❌ [openssl] Missing dependencies: {cmds} +deps_ok=✅ [openssl] All dependencies are available diff --git a/core/modules/openssl.tr.es b/core/modules/openssl.tr.es new file mode 100644 index 0000000..0b86cc0 --- /dev/null +++ b/core/modules/openssl.tr.es @@ -0,0 +1,20 @@ +missing_convert=❌ [openssl] Faltan argumentos para conversión: src, dest, format +missing_inspect=❌ [openssl] Archivo no encontrado para inspección: {src} +missing_trust=❌ [openssl] Faltan argumentos para instalación como CA: src, alias, trust_path +missing_untrust=❌ [openssl] Faltan argumentos para eliminación: alias, trust_path +src_not_found=❌ [openssl] Archivo no encontrado: {src} +converting=🔐 [openssl] Convirtiendo {src} a formato {format}... +converted=✅ [openssl] Conversión completada: {dest} +key_extracted=✅ [openssl] Clave extraída: {dest} +cer_converted=✅ [openssl] Certificado convertido a .cer: {dest} +unsupported_format=❌ [openssl] Formato de destino no soportado: {format} +inspecting=🔍 [openssl] Inspeccionando certificado: {src} +inspect_fail=⚠️ [openssl] No se pudo extraer información +trusting=🏛️ [openssl] Instalando certificado '{alias}' como CA... +trusted=✅ [openssl] Certificado instalado y CA actualizada +untrust_not_found=⚠️ [openssl] Certificado '{alias}' no encontrado en {trust_path} +untrusting=🧹 [openssl] Eliminando certificado '{alias}' de CA confiables... +untrusted=✅ [openssl] Certificado eliminado y CA actualizada +unknown_state=❌ [openssl] Estado desconocido: '{state}'. Usa 'convert', 'inspect', 'trust' o 'untrust' +missing_deps=❌ [openssl] Dependencias faltantes: {cmds} +deps_ok=✅ [openssl] Todas las dependencias están disponibles diff --git a/core/modules/package.sh b/core/modules/package.sh new file mode 100644 index 0000000..7e41161 --- /dev/null +++ b/core/modules/package.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Module: package +# Description: Instala, actualiza o elimina paquetes .deb/.rpm y permite actualizar el sistema +# License: GPLv3 +# Author: Luis GuLo +# Version: 2.2.0 +# Dependencies: ssh + +package_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local name="${args[name]:-}" + local state="${args[state]:-present}" + local become="${args[become]:-false}" + local update_type="${args[update_type]:-full}" # full | security + + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/package.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + fi + + echo "$(render_msg "${tr[start]}" "state=$state" "name=${name:-}")" + + local pkg_mgr + pkg_mgr=$(ssh "$host" "command -v apt-get || command -v apt || command -v dnf || command -v yum") + + if [ -z "$pkg_mgr" ]; then + echo "${tr[no_pkg_mgr]:-❌ [package] No se detectó gestor de paquetes compatible en el host.}" + return 1 + fi + + case "$pkg_mgr" in + *apt*) + if [ "$state" = "system-update" ]; then + system_update_apt "$host" "$prefix" + else + package_apt "$host" "$name" "$state" "$prefix" + fi + ;; + *yum*|*dnf*) + if [ "$state" = "system-update" ]; then + system_update_rpm "$host" "$prefix" "$update_type" + else + package_rpm "$host" "$name" "$state" "$prefix" + fi + ;; + *) + echo "$(render_msg "${tr[unsupported_mgr]}" "mgr=$pkg_mgr")" + return 1 + ;; + esac +} + +package_apt() { + local host="$1" + local name="$2" + local state="$3" + local prefix="$4" + + local check_cmd="dpkg -s '$name' &> /dev/null" + local install_cmd="$prefix apt-get update && $prefix apt-get install -y '$name'" + local remove_cmd="$prefix apt-get remove -y '$name'" + local upgrade_cmd="$prefix apt-get update && $prefix apt-get install --only-upgrade -y '$name'" + + case "$state" in + present) ssh "$host" "$check_cmd || $install_cmd" ;; + absent) ssh "$host" "$check_cmd && $remove_cmd" ;; + latest) ssh "$host" "$check_cmd && $upgrade_cmd || $install_cmd" ;; + *) echo "$(render_msg "${tr[unsupported_state_apt]}" "state=$state")"; return 1 ;; + esac +} + +package_rpm() { + local host="$1" + local name="$2" + local state="$3" + local prefix="$4" + + local check_cmd="rpm -q '$name' &> /dev/null" + local install_cmd="$prefix yum install -y '$name' || $prefix dnf install -y '$name'" + local remove_cmd="$prefix yum remove -y '$name' || $prefix dnf remove -y '$name'" + local upgrade_cmd="$prefix yum update -y '$name' || $prefix dnf upgrade -y '$name'" + + case "$state" in + present) ssh "$host" "$check_cmd || $install_cmd" ;; + absent) ssh "$host" "$check_cmd && $remove_cmd" ;; + latest) ssh "$host" "$check_cmd && $upgrade_cmd || $install_cmd" ;; + *) echo "$(render_msg "${tr[unsupported_state_rpm]}" "state=$state")"; return 1 ;; + esac +} + +system_update_apt() { + local host="$1" + local prefix="$2" + echo "${tr[update_apt]:-🔄 [package] Actualización completa del sistema (.deb)}" + ssh "$host" "$prefix apt-get update && $prefix apt-get upgrade -y" +} + +system_update_rpm() { + local host="$1" + local prefix="$2" + local update_type="$3" + + if [ "$update_type" = "security" ]; then + echo "${tr[update_rpm_security]:-🔐 [package] Actualización de seguridad (.rpm)}" + ssh "$host" "$prefix dnf update --security -y || $prefix yum update --security -y" + else + echo "${tr[update_rpm_full]:-🔄 [package] Actualización completa del sistema (.rpm)}" + ssh "$host" "$prefix dnf upgrade --refresh -y || $prefix yum update -y" + fi +} + +check_dependencies_package() { + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [package] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [package] ssh disponible.}" + return 0 +} diff --git a/core/modules/package.tr.en b/core/modules/package.tr.en new file mode 100644 index 0000000..baae9c5 --- /dev/null +++ b/core/modules/package.tr.en @@ -0,0 +1,10 @@ +start=📦 [package] State: {state} | Package: {name} | Manager: detecting... +no_pkg_mgr=❌ [package] No compatible package manager detected on host. +unsupported_mgr=❌ [package] Manager '{mgr}' not supported. +unsupported_state_apt=❌ [package] State '{state}' not supported for APT. +unsupported_state_rpm=❌ [package] State '{state}' not supported for RPM. +update_apt=🔄 [package] Full system update (.deb) +update_rpm_security=🔐 [package] Security update (.rpm) +update_rpm_full=🔄 [package] Full system update (.rpm) +missing_deps=❌ [package] ssh is not available. +deps_ok=✅ [package] ssh is available. diff --git a/core/modules/package.tr.es b/core/modules/package.tr.es new file mode 100644 index 0000000..ed7c4fa --- /dev/null +++ b/core/modules/package.tr.es @@ -0,0 +1,10 @@ +start=📦 [package] Estado: {state} | Paquete: {name} | Gestor: detectando... +no_pkg_mgr=❌ [package] No se detectó gestor de paquetes compatible en el host. +unsupported_mgr=❌ [package] Gestor '{mgr}' no soportado. +unsupported_state_apt=❌ [package] Estado '{state}' no soportado para APT. +unsupported_state_rpm=❌ [package] Estado '{state}' no soportado para RPM. +update_apt=🔄 [package] Actualización completa del sistema (.deb) +update_rpm_security=🔐 [package] Actualización de seguridad (.rpm) +update_rpm_full=🔄 [package] Actualización completa del sistema (.rpm) +missing_deps=❌ [package] ssh no está disponible. +deps_ok=✅ [package] ssh disponible. diff --git a/core/modules/ping.sh b/core/modules/ping.sh new file mode 100644 index 0000000..79b72c6 --- /dev/null +++ b/core/modules/ping.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Module: ping +# Description: Verifica conectividad desde el host remoto hacia un destino específico +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2 +# Dependencies: ping, ssh + +ping_task() { + local host="$1"; shift + declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local count="${args[count]:-2}" + local timeout="${args[timeout]:-3}" + local target="${args[target]:-127.0.0.1}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ping.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + else + echo "⚠️ [ping] Archivo de traducción no encontrado: $trfile" + fi + + echo "$(render_msg "${tr[start]}" "host=$host" "target=$target")" + + if ssh "$host" "$prefix ping -c $count -W $timeout $target &>/dev/null"; then + echo " $(render_msg "${tr[success]}" "host=$host" "target=$target")" + return 0 + else + echo " $(render_msg "${tr[fail]}" "host=$host" "target=$target")" + return 1 + fi +} + +check_dependencies_ping() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/ping.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null || ! command -v ping &> /dev/null; then + echo " ${tr[missing_deps]:-❌ [ping] ssh o ping no están disponibles.}" + return 1 + fi + echo " ${tr[deps_ok]:-✅ [ping] ssh y ping disponibles.}" + return 0 +} diff --git a/core/modules/ping.tr.en b/core/modules/ping.tr.en new file mode 100644 index 0000000..1faf4fe --- /dev/null +++ b/core/modules/ping.tr.en @@ -0,0 +1,5 @@ +start=📡 [ping] Testing connectivity from {host} to {target}... +success=✅ [ping] {host} can reach {target} +fail=❌ [ping] {host} cannot reach {target} +missing_deps=❌ [ping] ssh or ping are not available. +deps_ok=✅ [ping] ssh and ping are available. diff --git a/core/modules/ping.tr.es b/core/modules/ping.tr.es new file mode 100644 index 0000000..37c3805 --- /dev/null +++ b/core/modules/ping.tr.es @@ -0,0 +1,5 @@ +start=📡 [ping] Probando conectividad desde {host} hacia {target}... +success=✅ [ping] {host} puede alcanzar {target} +fail=❌ [ping] {host} no puede alcanzar {target} +missing_deps=❌ [ping] ssh o ping no están disponibles. +deps_ok=✅ [ping] ssh y ping disponibles. diff --git a/core/modules/replace.sh b/core/modules/replace.sh new file mode 100644 index 0000000..fb961ea --- /dev/null +++ b/core/modules/replace.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Module: replace +# Description: Reemplaza texto en archivos usando expresiones regulares +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: sed, cp, tee + +replace_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local path="${args[path]}" + local regexp="${args[regexp]}" + local replace="${args[replace]}" + local backup="${args[backup]:-true}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/replace.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ ! -f "$path" ]]; then + echo "$(render_msg "${tr[missing_file]}" "path=$path")" + return 1 + fi + + if [[ "$backup" == "true" ]]; then + cp "$path" "$path.bak" + echo "$(render_msg "${tr[backup_created]}" "path=$path")" + fi + + $prefix sed -i "s|$regexp|$replace|g" "$path" + echo "$(render_msg "${tr[replaced]}" "path=$path")" +} + +check_dependencies_replace() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/replace.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in sed cp tee; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [replace] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/replace.tr.en b/core/modules/replace.tr.en new file mode 100644 index 0000000..f4ec4ae --- /dev/null +++ b/core/modules/replace.tr.en @@ -0,0 +1,5 @@ +missing_file=❌ [replace] File not found: {path} +backup_created=📦 Backup created: {path}.bak +replaced=✅ [replace] Replacement applied to: {path} +missing_deps=❌ [replace] Missing dependencies: {cmds} +deps_ok=✅ [replace] All dependencies are available diff --git a/core/modules/replace.tr.es b/core/modules/replace.tr.es new file mode 100644 index 0000000..f6e647e --- /dev/null +++ b/core/modules/replace.tr.es @@ -0,0 +1,5 @@ +missing_file=❌ [replace] El archivo no existe: {path} +backup_created=📦 Copia de seguridad creada: {path}.bak +replaced=✅ [replace] Reemplazo aplicado en: {path} +missing_deps=❌ [replace] Dependencias faltantes: {cmds} +deps_ok=✅ [replace] Todas las dependencias están disponibles diff --git a/core/modules/run.sh b/core/modules/run.sh new file mode 100644 index 0000000..4ec4e9b --- /dev/null +++ b/core/modules/run.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Module: run +# Description: Ejecuta comandos remotos vía SSH, con soporte para vault y sudo +# License: GPLv3 +# Author: Luis GuLo +# Version: 2.0.0 +# Dependencies: ssh, core/utils/vault_utils.sh + +# Detectar raíz del proyecto si no está definida +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + +# Cargar utilidades +source "$PROJECT_ROOT/core/utils/vault_utils.sh" + +run_task() { + local host="$1"; shift + declare -A args + + while [[ "$#" -gt 0 ]]; do + case "$1" in + *=*) + key="${1%%=*}" + value="${1#*=}" + args["$key"]="$value" + ;; + esac + shift + done + + local command="${args[command]}" + local become="${args[become]:-}" + local vault_key="${args[vault_key]:-}" + + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/run.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$val"; done < "$trfile" + fi + + # 🧠 Comandos que no deben ejecutarse con sudo + local safe_cmds=("echo" "true" "false" "command" "which" "exit" "test") + local first_cmd="${command%% *}" + for safe in "${safe_cmds[@]}"; do + if [[ "$first_cmd" == "$safe" ]]; then + prefix="" + break + fi + done + + # 🔁 Interpolación de variables ShFlow + for var in $(compgen -A variable | grep '^shflow_vars_'); do + key="${var#shflow_vars_}" + value="${!var}" + command="${command//\{\{ $key \}\}/$value}" + done + + echo "$(render_msg "${tr[start]}" "host=$host" "command=$command" "prefix=$prefix")" + + if [ -n "$vault_key" ]; then + local secret + secret=$(get_secret "$vault_key") || { + echo "$(render_msg "${tr[vault_fail]}" "vault_key=$vault_key")" + return 1 + } + ssh "$host" "$prefix TOKEN='$secret' $command" + else + ssh "$host" "$prefix $command" + fi +} + +check_dependencies_run() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/run.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_deps]:-❌ [run] ssh no está disponible.}" + return 1 + fi + echo "${tr[deps_ok]:-✅ [run] ssh disponible.}" + return 0 +} diff --git a/core/modules/run.tr.en b/core/modules/run.tr.en new file mode 100644 index 0000000..55a3f3e --- /dev/null +++ b/core/modules/run.tr.en @@ -0,0 +1,4 @@ +start=📡 [run] Executing on {host}: {prefix} {command} +vault_fail=❌ [run] Failed to retrieve secret '{vault_key}' +missing_deps=❌ [run] ssh is not available. +deps_ok=✅ [run] ssh is available. diff --git a/core/modules/run.tr.es b/core/modules/run.tr.es new file mode 100644 index 0000000..20b7f2f --- /dev/null +++ b/core/modules/run.tr.es @@ -0,0 +1,4 @@ +start=📡 [run] Ejecutando en {host}: {prefix} {command} +vault_fail=❌ [run] No se pudo obtener el secreto '{vault_key}' +missing_deps=❌ [run] ssh no está disponible. +deps_ok=✅ [run] ssh disponible. diff --git a/core/modules/service.sh b/core/modules/service.sh new file mode 100644 index 0000000..bda0cd3 --- /dev/null +++ b/core/modules/service.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Module: service +# Description: Controla servicios del sistema remoto (start, stop, restart, enable, disable) con idempotencia +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: ssh, systemctl + +service_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local name="${args[name]}" + local state="${args[state]}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/service.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$state" in + start|stop|restart|enable|disable) + echo "$(render_msg "${tr[executing]}" "state=$state" "name=$name" "host=$host")" + ssh "$host" "$prefix systemctl $state '$name'" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_service() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/service.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if ! command -v ssh &> /dev/null; then + echo "${tr[missing_ssh]:-❌ [service] ssh no está disponible.}" + return 1 + fi + echo "${tr[ssh_ok]:-✅ [service] ssh disponible.}" + + if ! command -v systemctl &> /dev/null; then + echo "${tr[missing_systemctl]:-⚠️ [service] systemctl no está disponible localmente. Se asumirá que existe en el host remoto.}" + else + echo "${tr[systemctl_ok]:-✅ [service] systemctl disponible localmente.}" + fi + return 0 +} diff --git a/core/modules/service.tr.en b/core/modules/service.tr.en new file mode 100644 index 0000000..87dc131 --- /dev/null +++ b/core/modules/service.tr.en @@ -0,0 +1,6 @@ +executing=🔧 [service] Executing '{state}' on service '{name}' at {host} +unsupported=❌ [service] State '{state}' not supported. Use start, stop, restart, enable or disable. +missing_ssh=❌ [service] ssh is not available. +ssh_ok=✅ [service] ssh is available. +missing_systemctl=⚠️ [service] systemctl is not available locally. Assuming it exists on the remote host. +systemctl_ok=✅ [service] systemctl is available locally. diff --git a/core/modules/service.tr.es b/core/modules/service.tr.es new file mode 100644 index 0000000..26c4032 --- /dev/null +++ b/core/modules/service.tr.es @@ -0,0 +1,6 @@ +executing=🔧 [service] Ejecutando '{state}' sobre el servicio '{name}' en {host} +unsupported=❌ [service] Estado '{state}' no soportado. Usa start, stop, restart, enable o disable. +missing_ssh=❌ [service] ssh no está disponible. +ssh_ok=✅ [service] ssh disponible. +missing_systemctl=⚠️ [service] systemctl no está disponible localmente. Se asumirá que existe en el host remoto. +systemctl_ok=✅ [service] systemctl disponible localmente. diff --git a/core/modules/smtp_send.sh b/core/modules/smtp_send.sh new file mode 100644 index 0000000..6961ec4 --- /dev/null +++ b/core/modules/smtp_send.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Module: smtp_send +# Description: Envía un correo de prueba usando SMTP con netcat o openssl s_client +# License: GPLv3 +# Author: Luis GuLo +# Version: 0.2.0 +# Dependencies: nc o openssl, base64 + +smtp_send_task() { + declare -A args + for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local smtp_server="${args[smtp_server]}" + local smtp_port="${args[smtp_port]:-587}" + local smtp_user="${args[smtp_user]}" + local smtp_pass="${args[smtp_pass]}" + local from="${args[from]}" + local to="${args[to]}" + local subject="${args[subject]:-Prueba desde ShFlow}" + local body="${args[body]:-Este es un correo de prueba enviado desde el módulo smtp_send.}" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/smtp_send.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if [[ -z "$smtp_server" || -z "$smtp_user" || -z "$smtp_pass" || -z "$from" || -z "$to" ]]; then + echo "${tr[missing_args]:-❌ [smtp_send] Faltan argumentos obligatorios: smtp_server, smtp_user, smtp_pass, from, to}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "to=$to" "server=$smtp_server" "port=$smtp_port")" + + local auth_user auth_pass + auth_user=$(echo -n "$smtp_user" | base64) + auth_pass=$(echo -n "$smtp_pass" | base64) + + local smtp_script + smtp_script=$(cat < +RCPT TO:<$to> +DATA +Subject: $subject +From: $from +To: $to + +$body +. +QUIT +EOF +) + + if command -v nc &>/dev/null; then + echo "${tr[using_nc]:-🔧 Usando netcat para conexión SMTP...}" + echo "$smtp_script" | nc "$smtp_server" "$smtp_port" + elif command -v openssl &>/dev/null; then + echo "${tr[using_openssl]:-🔧 Usando openssl para conexión STARTTLS...}" + echo "$smtp_script" | openssl s_client -starttls smtp -crlf -connect "$smtp_server:$smtp_port" 2>/dev/null + else + echo "${tr[missing_tools]:-❌ [smtp_send] No se encontró ni netcat (nc) ni openssl en el sistema.}" + return 1 + fi + + echo "${tr[done]:-✅ [smtp_send] Comando ejecutado. Verifica si el correo fue recibido.}" +} + +check_dependencies_smtp_send() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/smtp_send.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + if command -v nc &>/dev/null || command -v openssl &>/dev/null; then + if ! command -v base64 &>/dev/null; then + echo "${tr[missing_base64]:-❌ [smtp_send] Falta base64 en el sistema.}" + return 1 + fi + local tool=$(command -v nc &>/dev/null && echo "nc" || echo "openssl") + echo "$(render_msg "${tr[deps_ok]}" "tool=$tool")" + return 0 + else + echo "${tr[missing_tools]:-❌ [smtp_send] No se encontró ni nc ni openssl.}" + return 1 + fi +} diff --git a/core/modules/smtp_send.tr.en b/core/modules/smtp_send.tr.en new file mode 100644 index 0000000..f99ba99 --- /dev/null +++ b/core/modules/smtp_send.tr.en @@ -0,0 +1,8 @@ +missing_args=❌ [smtp_send] Missing required arguments: smtp_server, smtp_user, smtp_pass, from, to +start=📡 [smtp_send] Preparing to send to {to} via {server}:{port}... +using_nc=🔧 Using netcat for SMTP connection... +using_openssl=🔧 Using openssl for STARTTLS connection... +missing_tools=❌ [smtp_send] Neither netcat (nc) nor openssl found on system. +missing_base64=❌ [smtp_send] base64 is missing on the system. +deps_ok=✅ [smtp_send] Tools available: {tool}, base64 +done=✅ [smtp_send] Command executed. Check if the email was received. diff --git a/core/modules/smtp_send.tr.es b/core/modules/smtp_send.tr.es new file mode 100644 index 0000000..4fa0b99 --- /dev/null +++ b/core/modules/smtp_send.tr.es @@ -0,0 +1,8 @@ +missing_args=❌ [smtp_send] Faltan argumentos obligatorios: smtp_server, smtp_user, smtp_pass, from, to +start=📡 [smtp_send] Preparando envío a {to} vía {server}:{port}... +using_nc=🔧 Usando netcat para conexión SMTP... +using_openssl=🔧 Usando openssl para conexión STARTTLS... +missing_tools=❌ [smtp_send] No se encontró ni netcat (nc) ni openssl en el sistema. +missing_base64=❌ [smtp_send] Falta base64 en el sistema. +deps_ok=✅ [smtp_send] Herramientas disponibles: {tool}, base64 +done=✅ [smtp_send] Comando ejecutado. Verifica si el correo fue recibido. diff --git a/core/modules/template.sh b/core/modules/template.sh new file mode 100644 index 0000000..887a47b --- /dev/null +++ b/core/modules/template.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Module: template +# Description: Genera archivos a partir de plantillas con variables {{var}}, bucles, includes y delimitadores configurables +# Author: Luis GuLo +# Version: 0.4.0 +# Dependencies: bash, sed, tee, grep, cat + +# 🧭 Detección de raíz del proyecto +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +TEMPLATE_DIR="$PROJECT_ROOT/core/templates" + +template_task() { + local host="$1"; shift + declare -A args + local start_delim="{{" end_delim="}}" strict="false" + declare -A vars + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/template.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # Parsear argumentos + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + case "$key" in + src) src="$value" ;; + dest) dest="$value" ;; + become) become="$value" ;; + delimiters) + start_delim="${value%% *}" + end_delim="${value#* }" + ;; + strict) strict="$value" ;; + *) vars["$key"]="$value" ;; + esac + done + + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + local template_path="$TEMPLATE_DIR/$src" + [[ ! -f "$template_path" ]] && echo "$(render_msg "${tr[missing_template]}" "path=$template_path")" && return 1 + + local rendered="" + local line loop_active="false" loop_key="" loop_buffer=() + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ${start_delim}[[:space:]]*include[[:space:]]*\"([^\"]+)\"[[:space:]]*${end_delim} ]]; then + include_file="${BASH_REMATCH[1]}" + include_path="$TEMPLATE_DIR/$include_file" + [[ -f "$include_path" ]] && rendered+=$(cat "$include_path")$'\n' + continue + fi + + if [[ "$line" =~ ^#LOOP[[:space:]]+([a-zA-Z0-9_]+)$ ]]; then + loop_active="true" + loop_key="${BASH_REMATCH[1]}" + loop_buffer=() + continue + fi + + if [[ "$line" == "#ENDLOOP" ]]; then + loop_active="false" + IFS=',' read -r -a items <<< "${vars[$loop_key]}" + for item in "${items[@]}"; do + for loop_line in "${loop_buffer[@]}"; do + rendered+=$(replace_vars "$loop_line" "$item" "$start_delim" "$end_delim")$'\n' + done + done + continue + fi + + if [[ "$loop_active" == "true" ]]; then + loop_buffer+=("$line") + continue + fi + + rendered+=$(replace_vars "$line" "" "$start_delim" "$end_delim")$'\n' + done < "$template_path" + + if [[ "$strict" == "true" ]]; then + missing=$(echo "$rendered" | grep -o "${start_delim}[^${end_delim}]*${end_delim}" | sort -u) + if [[ -n "$missing" ]]; then + echo "${tr[missing_vars]:-❌ [template] Variables no definidas:}" + echo "$missing" + return 1 + fi + fi + + echo "$rendered" | $prefix tee "$dest" > /dev/null + echo "$(render_msg "${tr[generated]}" "dest=$dest")" +} + +replace_vars() { + local line="$1" + local item="$2" + local start_delim="$3" + local end_delim="$4" + for key in "${!vars[@]}"; do + line="${line//${start_delim}${key}${end_delim}/${vars[$key]}}" + done + [[ -n "$item" ]] && line="${line//${start_delim}item${end_delim}/$item}" + echo "$line" +} + +check_dependencies_template() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/template.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in sed tee grep cat; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [template] Todas las dependencias están disponibles}" + return 0 +} diff --git a/core/modules/template.tr.en b/core/modules/template.tr.en new file mode 100644 index 0000000..8c985d8 --- /dev/null +++ b/core/modules/template.tr.en @@ -0,0 +1,5 @@ +missing_template=❌ [template] Template not found: {path} +missing_vars=❌ [template] Undefined variables: +generated=✅ [template] File generated: {dest} +missing_deps=❌ [template] Missing dependencies: {cmds} +deps_ok=✅ [template] All dependencies are available diff --git a/core/modules/template.tr.es b/core/modules/template.tr.es new file mode 100644 index 0000000..f9bdff4 --- /dev/null +++ b/core/modules/template.tr.es @@ -0,0 +1,5 @@ +missing_template=❌ [template] Plantilla no encontrada: {path} +missing_vars=❌ [template] Variables no definidas: +generated=✅ [template] Archivo generado: {dest} +missing_deps=❌ [template] Dependencias faltantes: {cmds} +deps_ok=✅ [template] Todas las dependencias están disponibles diff --git a/core/modules/user.tr.en b/core/modules/user.tr.en new file mode 100644 index 0000000..9e1d177 --- /dev/null +++ b/core/modules/user.tr.en @@ -0,0 +1,15 @@ +priv_required=❌ [users] Privileges required to manage users. Use 'become: true'. +missing_username=❌ [users] Missing required parameter 'username' +enter_create=🔧 [users] Entering create +enter_modify=🔧 [users] Entering modify +enter_absent=🔧 [users] Entering absent +exists=✅ [users] User '{username}' already exists +not_exists=⚠️ [users] User '{username}' does not exist +group_create=🔧 [users] Creating group '{groups}' +created=✅ [users] User '{username}' created +modified=✅ [users] User '{username}' modified +deleted=✅ [users] User '{username}' deleted +already_deleted=✅ [users] User '{username}' already deleted +unsupported_state=❌ [users] State '{state}' not supported. Use create, modify or absent. +missing_deps=❌ [users] Missing dependencies: {cmds} +deps_ok=✅ [users] All dependencies are present diff --git a/core/modules/user.tr.es b/core/modules/user.tr.es new file mode 100644 index 0000000..1820c74 --- /dev/null +++ b/core/modules/user.tr.es @@ -0,0 +1,15 @@ +priv_required=❌ [users] Se requieren privilegios para gestionar usuarios. Usa 'become: true'. +missing_username=❌ [users] Falta el parámetro obligatorio 'username' +enter_create=🔧 [users] Entrando en create +enter_modify=🔧 [users] Entrando en modify +enter_absent=🔧 [users] Entrando en absent +exists=✅ [users] Usuario '{username}' ya existe +not_exists=⚠️ [users] Usuario '{username}' no existe +group_create=🔧 [users] Creando grupo '{groups}' +created=✅ [users] Usuario '{username}' creado +modified=✅ [users] Usuario '{username}' modificado +deleted=✅ [users] Usuario '{username}' eliminado +already_deleted=✅ [users] Usuario '{username}' ya eliminado +unsupported_state=❌ [users] Estado '{state}' no soportado. Usa create, modify o absent. +missing_deps=❌ [users] Dependencias faltantes: {cmds} +deps_ok=✅ [users] Todas las dependencias están presentes diff --git a/core/modules/users.sh b/core/modules/users.sh new file mode 100644 index 0000000..e7306e2 --- /dev/null +++ b/core/modules/users.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Module: users +# Description: Gestiona usuarios del sistema (crear, modificar, eliminar) +# Author: Luis GuLo +# Version: 1.4.0 +# Dependencies: id, useradd, usermod, userdel, groupadd, sudo + +users_task() { + local host="$1"; shift + declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local username="${args[username]}" + local home="${args[home]:-/home/$username}" + local shell="${args[shell]:-/bin/bash}" + local groups="${args[groups]:-}" + local state="${args[state]:-create}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # 🌐 Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/users.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + # 🛡️ Validación + if [[ "$become" != "true" && "$EUID" -ne 0 ]]; then + echo "${tr[priv_required]:-❌ [users] Se requieren privilegios para gestionar usuarios. Usa 'become: true'.}" + return 1 + fi + + if [[ -z "$username" ]]; then + echo "${tr[missing_username]:-❌ [users] Falta el parámetro obligatorio 'username'}" + return 1 + fi + + case "$state" in + create) + echo "${tr[enter_create]:-🔧 [users] Entrando en create}" + if id "$username" &>/dev/null; then + echo "$(render_msg "${tr[exists]}" "username=$username")" + return 0 + fi + if [[ -n "$groups" && "$groups" != "$username" ]]; then + if ! getent group "$groups" &>/dev/null; then + echo "$(render_msg "${tr[group_create]}" "groups=$groups")" + $prefix groupadd "$groups" + fi + fi + local cmd="$prefix useradd -m \"$username\" -s \"$shell\" -d \"$home\"" + [[ -n "$groups" ]] && cmd="$cmd -G \"$groups\"" + eval "$cmd" && echo "$(render_msg "${tr[created]}" "username=$username")" + ;; + modify) + echo "${tr[enter_modify]:-🔧 [users] Entrando en modify}" + if ! id "$username" &>/dev/null; then + echo "$(render_msg "${tr[not_exists]}" "username=$username")" + return 1 + fi + local cmd="$prefix usermod \"$username\"" + [[ -n "$shell" ]] && cmd="$cmd -s \"$shell\"" + [[ -n "$home" ]] && cmd="$cmd -d \"$home\"" + [[ -n "$groups" ]] && cmd="$cmd -G \"$groups\"" + eval "$cmd" && echo "$(render_msg "${tr[modified]}" "username=$username")" + ;; + absent) + echo "${tr[enter_absent]:-🔧 [users] Entrando en absent}" + if ! id "$username" &>/dev/null; then + echo "$(render_msg "${tr[already_deleted]}" "username=$username")" + return 0 + fi + eval "$prefix userdel -r \"$username\"" && echo "$(render_msg "${tr[deleted]}" "username=$username")" + ;; + *) + echo "$(render_msg "${tr[unsupported_state]}" "state=$state")" + return 1 + ;; + esac +} + +check_dependencies_users() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/users.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + local missing=() + for cmd in id sudo; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing+=("$cmd") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "$(render_msg "${tr[missing_deps]}" "cmds=${missing[*]}")" + return 1 + fi + + echo "${tr[deps_ok]:-✅ [users] Todas las dependencias están presentes}" + return 0 +} diff --git a/core/modules/vault-remote.sh b/core/modules/vault-remote.sh new file mode 100644 index 0000000..4b3a1c6 --- /dev/null +++ b/core/modules/vault-remote.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Module: vault-remote +# Description: Sincroniza secretos cifrados entre vault local y remoto +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: ssh, scp, gpg + +VAULT_DIR="core/vault" + +vault_remote_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local action="${args[action]}" + local key="${args[key]}" + local remote_path="${args[remote_path]:-/tmp/shflow_vault}" + local become="${args[become]}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + # Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/vault-remote.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + case "$action" in + push) + if [ ! -f "$VAULT_DIR/$key.gpg" ]; then + echo "$(render_msg "${tr[missing_local]}" "key=$key")" + return 1 + fi + scp "$VAULT_DIR/$key.gpg" "$host:$remote_path/$key.gpg" + ssh "$host" "$prefix mkdir -p '$remote_path'" + echo "$(render_msg "${tr[pushed]}" "key=$key" "host=$host" "path=$remote_path")" + ;; + pull) + ssh "$host" "$prefix test -f '$remote_path/$key.gpg'" || { + echo "$(render_msg "${tr[missing_remote]}" "key=$key")" + return 1 + } + scp "$host:$remote_path/$key.gpg" "$VAULT_DIR/$key.gpg" + echo "$(render_msg "${tr[pulled]}" "key=$key" "host=$host")" + ;; + sync) + ssh "$host" "$prefix mkdir -p '$remote_path'" + scp "$VAULT_DIR/"*.gpg "$host:$remote_path/" + echo "$(render_msg "${tr[synced]}" "host=$host" "path=$remote_path")" + ;; + *) + echo "$(render_msg "${tr[unsupported]}" "action=$action")" + return 1 + ;; + esac +} + +check_dependencies_vault_remote() { + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/vault-remote.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile" + fi + + for cmd in ssh scp gpg; do + if ! command -v "$cmd" &> /dev/null; then + echo "$(render_msg "${tr[missing_deps]}" "cmd=$cmd")" + return 1 + fi + done + echo "${tr[deps_ok]:-✅ [vault-remote] Dependencias disponibles.}" + return 0 +} diff --git a/core/modules/vault-remote.tr.en b/core/modules/vault-remote.tr.en new file mode 100644 index 0000000..cb0b856 --- /dev/null +++ b/core/modules/vault-remote.tr.en @@ -0,0 +1,8 @@ +missing_local=❌ [vault-remote] Secret '{key}' does not exist locally. +missing_remote=❌ [vault-remote] Secret '{key}' does not exist on remote host. +pushed=📤 Secret '{key}' sent to {host}:{path} +pulled=📥 Secret '{key}' retrieved from {host} +synced=🔄 Vault synchronized with {host}:{path} +unsupported=❌ [vault-remote] Action '{action}' not supported. +missing_deps=❌ [vault-remote] Command '{cmd}' not available. +deps_ok=✅ [vault-remote] Dependencies available. diff --git a/core/modules/vault-remote.tr.es b/core/modules/vault-remote.tr.es new file mode 100644 index 0000000..79a6106 --- /dev/null +++ b/core/modules/vault-remote.tr.es @@ -0,0 +1,8 @@ +missing_local=❌ [vault-remote] Secreto '{key}' no existe localmente. +missing_remote=❌ [vault-remote] Secreto '{key}' no existe en el host remoto. +pushed=📤 Secreto '{key}' enviado a {host}:{path} +pulled=📥 Secreto '{key}' recuperado desde {host} +synced=🔄 Vault sincronizado con {host}:{path} +unsupported=❌ [vault-remote] Acción '{action}' no soportada. +missing_deps=❌ [vault-remote] Comando '{cmd}' no disponible. +deps_ok=✅ [vault-remote] Dependencias disponibles. diff --git a/core/modules/wait.sh b/core/modules/wait.sh new file mode 100644 index 0000000..409dfbe --- /dev/null +++ b/core/modules/wait.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Module: wait +# Description: Pausa la ejecución durante un número de segundos (soporta decimales) +# Author: Luis GuLo +# Version: 1.2.0 +# Dependencies: sleep + +wait_task() { + local host="$1"; shift + declare -A args; for arg in "$@"; do key="${arg%%=*}"; value="${arg#*=}"; args["$key"]="$value"; done + + local seconds="${args[seconds]:-1}" + + # Cargar traducciones + local lang="${shflow_vars[language]:-es}" + local trfile="$(dirname "${BASH_SOURCE[0]}")/wait.tr.${lang}" + declare -A tr + if [[ -f "$trfile" ]]; then + while IFS='=' read -r key val; do tr["$key"]="$val"; done < "$trfile" + fi + + if ! [[ "$seconds" =~ ^[0-9]+([.][0-9]+)?$ ]]; then + echo "${tr[invalid]:-❌ [wait] El parámetro 'seconds' debe ser un número válido (entero o decimal)}" + return 1 + fi + + echo "$(render_msg "${tr[start]}" "seconds=$seconds")" + sleep "$seconds" + echo "${tr[done]:-✅ [wait] Pausa completada}" +} + +check_dependencies_wait() { + command -v sleep &>/dev/null || { + echo "${tr[missing_deps]:-❌ [wait] El comando 'sleep' no está disponible}" + return 1 + } + echo "${tr[deps_ok]:-✅ [wait] Dependencias OK}" + return 0 +} diff --git a/core/modules/wait.tr.en b/core/modules/wait.tr.en new file mode 100644 index 0000000..a603364 --- /dev/null +++ b/core/modules/wait.tr.en @@ -0,0 +1,5 @@ +invalid=❌ [wait] The 'seconds' parameter must be a valid number (integer or decimal) +start=⏳ [wait] Waiting for {seconds} seconds... +done=✅ [wait] Pause completed +missing_deps=❌ [wait] The 'sleep' command is not available +deps_ok=✅ [wait] Dependencies OK diff --git a/core/modules/wait.tr.es b/core/modules/wait.tr.es new file mode 100644 index 0000000..764ed3f --- /dev/null +++ b/core/modules/wait.tr.es @@ -0,0 +1,5 @@ +invalid=❌ [wait] El parámetro 'seconds' debe ser un número válido (entero o decimal) +start=⏳ [wait] Esperando {seconds} segundos... +done=✅ [wait] Pausa completada +missing_deps=❌ [wait] El comando 'sleep' no está disponible +deps_ok=✅ [wait] Dependencias OK diff --git a/core/templates/.gitignore b/core/templates/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/core/utils/eg.sh b/core/utils/eg.sh new file mode 100755 index 0000000..551c9d0 --- /dev/null +++ b/core/utils/eg.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +Qes="8J+lmiBIYXMgZW5jb250cmFkbyBlbCBodWV2byBkZSBwYXNjdWEuIMKhQnVlbiBvam8h·4pyoIE5vIHRvZG9zIGxvcyBzY3JpcHRzIHRpZW5lbiBhbG1hLi4uIHBlcm8gZXN0ZSBzw60u·8J+noCBFbCBtZWpvciBidWcgZXMgZWwgcXVlIG51bmNhIGV4aXN0acOzLg==·8J+QoyBTaEZsb3cgdGUgc2FsdWRhIGRlc2RlIGxhcyBzb21icmFzLg==·8J+TnCBMYSBhdXRvbWF0aXphY2nDs24gdGFtYmnDqW4gdGllbmUgcG9lc8OtYS4=" +Qen="8J+lmiBZb3UgZm91bmQgdGhlIEVhc3RlciBlZ2cuIFNoYXJwIGV5ZSE=·4pyoIE5vdCBhbGwgc2NyaXB0cyBoYXZlIHNvdWwuLi4gYnV0IHRoaXMgb25lIGRvZXMu·8J+noCBUaGUgYmVzdCBidWcgaXMgdGhlIG9uZSB0aGF0IG5ldmVyIGV4aXN0ZWQu·8J+QoyBTaEZsb3cgZ3JlZXRzIHlvdSBmcm9tIHRoZSBzaGFkb3dzLg==·8J+TnCBBdXRvbWF0aW9uIGhhcyBwb2V0cnkgdG9vLg==" +vhs=([0]="448b55f2" [1]="c9f66247" [2]="154f020f" [3]="0e931208" [4]="d2e2fa57") + +sheg() { + local P="$1" ; P=$((P+1)) + echo "――――――" + echo "$Qes" |awk -F '·' -v p="$P" '{print $p}' | base64 -d; echo + echo "$Qen" |awk -F '·' -v p="$P" '{print $p}' | base64 -d; echo + echo "――――――" +} + +main() { + [[ $# -lt 1 ]] && return 0 + local input="$1" ; local hsh + hsh=$(echo -n "$input" | md5sum | cut -c1-8) + for n in "${!vhs[@]}"; do + if [[ "$hsh" == "${vhs[$n]}" ]]; then + sheg "$n" ; break + fi + done + return 0 +} + +main "$@" diff --git a/core/utils/module-docgen.sh b/core/utils/module-docgen.sh new file mode 100755 index 0000000..f5461cd --- /dev/null +++ b/core/utils/module-docgen.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# ShFlow Module Documentation Generator +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.4.0 + +set -euo pipefail + +# 🧭 Detección de la raíz del proyecto +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +OUTPUT="$PROJECT_ROOT/docs/modules-list.md" +MODULE_DIRS=("$PROJECT_ROOT/core/modules" "$PROJECT_ROOT/user_modules" "$PROJECT_ROOT/community_modules") + +export SHFLOW_LANG="${SHFLOW_LANG:-es}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" + +trfile="$PROJECT_ROOT/core/utils/module-docgen.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +# 📝 Encabezado del documento +{ + echo "${tr[title]:-# 🧩 Módulos en ShFlow}" + echo "" + echo "**$(render_msg "${tr[generated]}" "date=$(date '+%Y-%m-%d %H:%M:%S')")**" + echo "" + echo "| ${tr[col_module]:-Módulo} | ${tr[col_desc]:-Descripción} | ${tr[col_type]:-Tipo} | ${tr[col_author]:-Autor} | ${tr[col_version]:-Versión} | ${tr[col_deps]:-Dependencias} |" + echo "|--------|-------------|------|-------|---------|--------------|" +} > "$OUTPUT" + +# 🔁 Procesar módulos +for dir in "${MODULE_DIRS[@]}"; do + [ -d "$dir" ] || continue + TYPE=$(echo "$dir" | sed "s#$PROJECT_ROOT/##g") + while IFS= read -r -d '' file; do + name=$(basename "$file" .sh) + desc=$(grep -E '^# Description:' "$file" | sed 's/^# Description:[[:space:]]*//') + author=$(grep -E '^# Author:' "$file" | sed 's/^# Author:[[:space:]]*//') + version=$(grep -E '^# Version:' "$file" | sed 's/^# Version:[[:space:]]*//') + deps=$(grep -E '^# Dependencies:' "$file" | sed 's/^# Dependencies:[[:space:]]*//') + + # Asegurar valor minimo + name=${name:-""} + desc=${desc:-""} + author=${author:-""} + version=${version:-""} + deps=${deps:-""} + + [[ -z "$name" ]] && continue + + echo "| $name | $desc | $TYPE | $author | $version | $deps |" >> "$OUTPUT" + done < <(find "$dir" -type f -name "*.sh" -print0) +done + +# 📌 Pie de página +{ + echo "" + echo "${tr[footer]:-_Para actualizar esta tabla, ejecuta: \`module-docgen\`_}" +} >> "$OUTPUT" + +echo "$(render_msg "${tr[done]}" "path=$OUTPUT")" diff --git a/core/utils/module-docgen.tr.en b/core/utils/module-docgen.tr.en new file mode 100644 index 0000000..644b2c3 --- /dev/null +++ b/core/utils/module-docgen.tr.en @@ -0,0 +1,10 @@ +title=# 🧩 Modules in ShFlow +generated=Automatically generated on {date} +col_module=Module +col_desc=Description +col_type=Type +col_author=Author +col_version=Version +col_deps=Dependencies +footer=_To update this table, run: `module-docgen`_ +done=✅ Documentation generated at {path} diff --git a/core/utils/module-docgen.tr.es b/core/utils/module-docgen.tr.es new file mode 100644 index 0000000..14b68fa --- /dev/null +++ b/core/utils/module-docgen.tr.es @@ -0,0 +1,10 @@ +title=# 🧩 Módulos en ShFlow +generated=Generado automáticamente el {date} +col_module=Módulo +col_desc=Descripción +col_type=Tipo +col_author=Autor +col_version=Versión +col_deps=Dependencias +footer=_Para actualizar esta tabla, ejecuta: `module-docgen`_ +done=✅ Documentación generada en {path} diff --git a/core/utils/module-template.sh b/core/utils/module-template.sh new file mode 100755 index 0000000..a38624b --- /dev/null +++ b/core/utils/module-template.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# ShFlow Module Template Generator +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +MODULE_NAME="${1:-}" +MODULE_DIR="$PROJECT_ROOT/core/modules" +MODULE_FILE="$MODULE_DIR/$MODULE_NAME.sh" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/core/utils/module-template.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +# 🧪 Validar entrada +if [[ -z "$MODULE_NAME" ]]; then + echo "${tr[usage]:-❌ Uso: module-template.sh }" + exit 1 +fi + +if [[ -f "$MODULE_FILE" ]]; then + echo "$(render_msg "${tr[exists]}" "name=$MODULE_NAME" "dir=$MODULE_DIR")" + exit 1 +fi + +mkdir -p "$MODULE_DIR" + +cat > "$MODULE_FILE" < +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.0 +# Dependencies: + +${MODULE_NAME}_task() { + local host="\$1"; shift + declare -A args + for arg in "\$@"; do + key="\${arg%%=*}" + value="\${arg#*=}" + args["\$key"]="\$value" + done + + echo "🚧 Ejecutando módulo '$MODULE_NAME' en \$host" + # Aquí va la lógica principal +} + +check_dependencies_$MODULE_NAME() { + # Verifica herramientas necesarias + for cmd in ; do + if ! command -v "\$cmd" &> /dev/null; then + echo " ❌ [$MODULE_NAME] Falta: \$cmd" + return 1 + fi + done + echo " ✅ [$MODULE_NAME] Dependencias OK" + return 0 +} +EOF + +chmod +x "$MODULE_FILE" +echo "$(render_msg "${tr[created]}" "name=$MODULE_NAME" "path=$MODULE_FILE")" diff --git a/core/utils/module-template.tr.en b/core/utils/module-template.tr.en new file mode 100644 index 0000000..bc948d7 --- /dev/null +++ b/core/utils/module-template.tr.en @@ -0,0 +1,3 @@ +usage=❌ Usage: module-template.sh +exists=⚠️ Module '{name}' already exists in {dir} +created=✅ Module '{name}' created at {path} diff --git a/core/utils/module-template.tr.es b/core/utils/module-template.tr.es new file mode 100644 index 0000000..4dc6e63 --- /dev/null +++ b/core/utils/module-template.tr.es @@ -0,0 +1,3 @@ +usage=❌ Uso: module-template.sh +exists=⚠️ El módulo '{name}' ya existe en {dir} +created=✅ Módulo '{name}' creado en {path} diff --git a/core/utils/shflow-check.sh b/core/utils/shflow-check.sh new file mode 100755 index 0000000..130eca0 --- /dev/null +++ b/core/utils/shflow-check.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# ShFlow Environment Checker +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.5.0 + +set -e + +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + +MODULE_PATHS=( + "$PROJECT_ROOT/core/modules" + "$PROJECT_ROOT/user_modules" + "$PROJECT_ROOT/community_modules" +) + +# 🧩 Cargar funciones comunes si no están disponibles +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +GLOBAL_TOOLS=("bash" "ssh" "scp" "git" "curl" "jq" "yq" "gpg") + +REQUIRED_PATHS=( + "$PROJECT_ROOT/core/modules" + "$PROJECT_ROOT/core/utils" + "$PROJECT_ROOT/core/inventory" + "$PROJECT_ROOT/examples" + "$PROJECT_ROOT/user_modules" + "$PROJECT_ROOT/community_modules" + "$PROJECT_ROOT/shflow.sh" + "$PROJECT_ROOT/vault.sh" +) + +# 🌐 Cargar traducciones +lang="${shflow_vars[language]:-es}" +trfile="$PROJECT_ROOT/core/utils/shflow-check.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +check_global_tools() { + echo "${tr[tools_header]:-🔍 Verificando herramientas globales...}" + local missing=0 + for tool in "${GLOBAL_TOOLS[@]}"; do + if ! command -v "$tool" &> /dev/null; then + echo "$(render_msg "${tr[tool_missing]}" "tool=$tool")" + missing=1 + else + echo "$(render_msg "${tr[tool_ok]}" "tool=$tool")" + fi + done + return $missing +} + +check_structure() { + echo "" + echo "${tr[structure_header]:-📁 Verificando estructura de ShFlow...}" + local missing=0 + for path in "${REQUIRED_PATHS[@]}"; do + if [ ! -e "$path" ]; then + echo "$(render_msg "${tr[path_missing]}" "path=$path")" + missing=1 + else + echo "$(render_msg "${tr[path_ok]}" "path=$path")" + fi + done + return $missing +} + +load_and_check_modules() { + echo "" + echo "${tr[modules_header]:-🔍 Verificando módulos ShFlow...}" + for dir in "${MODULE_PATHS[@]}"; do + [ -d "$dir" ] || continue + while IFS= read -r -d '' mod; do + source "$mod" + done < <(find "$dir" -type f -name "*.sh" -print0) + done + + for func in $(declare -F | awk '{print $3}' | grep '^check_dependencies_'); do + echo "" + echo "$(render_msg "${tr[checking_func]}" "func=$func")" + $func || echo "$(render_msg "${tr[func_warn]}" "func=$func")" + done +} + +main() { + echo "${tr[title]:-🧪 ShFlow Environment Check}" + echo "${tr[separator]:-=============================}" + + check_global_tools + check_structure + load_and_check_modules + + echo "" + echo "${tr[done]:-✅ Verificación completada.}" +} + +main "$@" diff --git a/core/utils/shflow-check.tr.en b/core/utils/shflow-check.tr.en new file mode 100644 index 0000000..6430ffd --- /dev/null +++ b/core/utils/shflow-check.tr.en @@ -0,0 +1,12 @@ +title=🧪 ShFlow Environment Check +separator=_____________________________ +tools_header=🔍 Checking global tools... +tool_missing=❌ {tool} not found +tool_ok=✅ {tool} available +structure_header=📁 Checking ShFlow structure... +path_missing=❌ Missing: {path} +path_ok=✅ Found: {path} +modules_header=🔍 Checking ShFlow modules... +checking_func=🔧 Running {func}... +func_warn=⚠️ Incomplete dependencies in {func} +done=✅ Check completed. diff --git a/core/utils/shflow-check.tr.es b/core/utils/shflow-check.tr.es new file mode 100644 index 0000000..983f318 --- /dev/null +++ b/core/utils/shflow-check.tr.es @@ -0,0 +1,12 @@ +title=🧪 ShFlow Environment Check +separator=_____________________________ +tools_header=🔍 Verificando herramientas globales... +tool_missing=❌ {tool} no encontrado +tool_ok=✅ {tool} disponible +structure_header=📁 Verificando estructura de ShFlow... +path_missing=❌ Falta: {path} +path_ok=✅ Encontrado: {path} +modules_header=🔍 Verificando módulos ShFlow... +checking_func=🔧 Ejecutando {func}... +func_warn=⚠️ Dependencias incompletas en {func} +done=✅ Verificación completada. diff --git a/core/utils/shflow-doc.sh b/core/utils/shflow-doc.sh new file mode 100755 index 0000000..861f1b9 --- /dev/null +++ b/core/utils/shflow-doc.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# ShFlow Doc Generator +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 + +set -e + +# ───────────────────────────────────────────── +# 🧭 Detección de la raíz del proyecto +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" + +# 🧩 Cargar funciones comunes si no están disponibles +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${shflow_vars[language]:-es}" +trfile="$PROJECT_ROOT/core/utils/shflow-doc.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +MODULE_PATHS=( + "$PROJECT_ROOT/core/modules" + "$PROJECT_ROOT/user_modules" + "$PROJECT_ROOT/community_modules" +) + +extract_metadata() { + local file="$1" + local module desc author version deps + + module=$(grep -m1 '^# Module:' "$file" | cut -d':' -f2- | xargs) + desc=$(grep -m1 '^# Description:' "$file" | cut -d':' -f2- | xargs) + author=$(grep -m1 '^# Author:' "$file" | cut -d':' -f2- | xargs) + version=$(grep -m1 '^# Version:' "$file" | cut -d':' -f2- | xargs) + deps=$(grep -m1 '^# Dependencies:' "$file" | cut -d':' -f2- | xargs) + + echo "$(render_msg "${tr[module]}" "name=$module")" + echo "$(render_msg "${tr[desc]}" "desc=$desc")" + echo "$(render_msg "${tr[author]}" "author=$author")" + echo "$(render_msg "${tr[version]}" "version=$version")" + echo "$(render_msg "${tr[deps]}" "deps=$deps")" + echo "${tr[separator]:- ————————————————————————}" +} + +main() { + echo "${tr[title]:-📚 ShFlow Modules Documentation}" + echo "${tr[separator_line]:-=================================}" + + for dir in "${MODULE_PATHS[@]}"; do + [ -d "$dir" ] || continue + ROUTE=$(echo "$dir" | sed "s#$PROJECT_ROOT/##g") + echo -e "\n$(render_msg "${tr[section]}" "type=$ROUTE")" + while IFS= read -r -d '' file; do + extract_metadata "$file" + done < <(find "$dir" -type f -name "*.sh" -print0) + done +} + +main "$@" diff --git a/core/utils/shflow-doc.tr.en b/core/utils/shflow-doc.tr.en new file mode 100644 index 0000000..5d3167e --- /dev/null +++ b/core/utils/shflow-doc.tr.en @@ -0,0 +1,9 @@ +title=📚 ShFlow Modules Documentation +separator_line================================== +section=🗃️ Module Type: {type} +module= 📦 Module: {name} +desc= 🔧 Description: {desc} +author= 👤 Author: {author} +version= 📌 Version: {version} +deps= 📎 Dependencies: {deps} +separator= ———————————————————————— diff --git a/core/utils/shflow-doc.tr.es b/core/utils/shflow-doc.tr.es new file mode 100644 index 0000000..b1150f7 --- /dev/null +++ b/core/utils/shflow-doc.tr.es @@ -0,0 +1,9 @@ +title=📚 ShFlow Modules Documentation +separator_line================================== +section=🗃️ Tipo de módulo: {type} +module= 📦 Módulo: {name} +desc= 🔧 Descripción: {desc} +author= 👤 Autor: {author} +version= 📌 Versión: {version} +deps= 📎 Dependencias: {deps} +separator= ———————————————————————— diff --git a/core/utils/shflow-ssh-init.sh b/core/utils/shflow-ssh-init.sh new file mode 100755 index 0000000..26d5e70 --- /dev/null +++ b/core/utils/shflow-ssh-init.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Utility: shflow-ssh-init +# Description: Inicializa acceso SSH sin contraseña en los hosts del inventario +# Author: Luis GuLo +# Version: 0.2.0 + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +INVENTORY="$PROJECT_ROOT/core/inventory/hosts.yaml" +TIMEOUT=5 +USER="${USER:-$(whoami)}" +KEY="${KEY:-$HOME/.ssh/id_rsa.pub}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +export SHFLOW_LANG="${SHFLOW_LANG:-es}" +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" + +trfile="$PROJECT_ROOT/core/utils/shflow-ssh-init.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +echo "$(render_msg "${tr[start]}" "user=$USER")" +echo "$(render_msg "${tr[inventory]}" "path=$INVENTORY")" +echo "$(render_msg "${tr[key]}" "key=$KEY")" +echo "" + +# 🧪 Validar dependencias +for cmd in yq ssh ssh-copy-id; do + if ! command -v "$cmd" &>/dev/null; then + echo "$(render_msg "${tr[missing_dep]}" "cmd=$cmd")" + exit 1 + fi +done + +# 🔁 Extraer hosts +HOSTS=() +HOSTS_RAW=$(yq ".all.hosts | keys | .[]" "$INVENTORY") +[ -z "$HOSTS_RAW" ] && echo "${tr[no_hosts]:-❌ No se encontraron hosts en el inventario.}" && exit 1 + +while IFS= read -r line; do + HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')") # Eliminar comillas +done <<< "$HOSTS_RAW" + +# 🔍 Evaluar cada host +for host in "${HOSTS[@]}"; do + IP=$(yq -r ".all.hosts.\"$host\".ansible_host" "$INVENTORY") + [[ "$IP" == "null" || -z "$IP" ]] && echo "$(render_msg "${tr[missing_ip]}" "host=$host")" && continue + + echo "$(render_msg "${tr[checking]}" "host=$host" "ip=$IP")" + + if ssh -o BatchMode=yes -o ConnectTimeout=$TIMEOUT "$USER@$IP" 'true' &>/dev/null; then + echo "${tr[skip]:- 🔁 Inicialización SSH no es necesaria}" + continue + fi + + echo "$(render_msg "${tr[copying]}" "user=$USER" "ip=$IP")" + if ssh-copy-id -i "$KEY" "$USER@$IP"; then + echo "${tr[success]:- ✅ Clave pública instalada correctamente}" + else + echo "${tr[fail]:- ❌ Fallo al instalar clave pública}" + fi + + echo "" +done + +echo "${tr[done]:-✅ Proceso de inicialización SSH completado}" diff --git a/core/utils/shflow-ssh-init.tr.en b/core/utils/shflow-ssh-init.tr.en new file mode 100644 index 0000000..1005b42 --- /dev/null +++ b/core/utils/shflow-ssh-init.tr.en @@ -0,0 +1,12 @@ +start=🔐 Initializing passwordless SSH access for user: {user} +inventory=📁 Inventory: {path} +key=🔑 Public key: {key} +missing_dep=❌ Requires '{cmd}' installed on the system +no_hosts=❌ No hosts found in inventory. +missing_ip=⚠️ Host '{host}' has no ansible_host defined +checking=🖥️ Host: {host} ({ip}) +skip= 🔁 SSH initialization not needed +copying= 🚀 Running ssh-copy-id for {user}@{ip} +success= ✅ Public key installed successfully +fail= ❌ Failed to install public key +done=✅ SSH initialization process completed diff --git a/core/utils/shflow-ssh-init.tr.es b/core/utils/shflow-ssh-init.tr.es new file mode 100644 index 0000000..c693c8f --- /dev/null +++ b/core/utils/shflow-ssh-init.tr.es @@ -0,0 +1,12 @@ +start=🔐 Inicializando acceso SSH sin contraseña para usuario: {user} +inventory=📁 Inventario: {path} +key=🔑 Clave pública: {key} +missing_dep=❌ Requiere '{cmd}' instalado en el sistema +no_hosts=❌ No se encontraron hosts en el inventario. +missing_ip=⚠️ Host '{host}' sin ansible_host definido +checking=🖥️ Host: {host} ({ip}) +skip= 🔁 Inicialización SSH no es necesaria +copying= 🚀 Ejecutando ssh-copy-id para {user}@{ip} +success= ✅ Clave pública instalada correctamente +fail= ❌ Fallo al instalar clave pública +done=✅ Proceso de inicialización SSH completado diff --git a/core/utils/shflow-trust.sh b/core/utils/shflow-trust.sh new file mode 100755 index 0000000..bc27d94 --- /dev/null +++ b/core/utils/shflow-trust.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Utility: shflow-trust +# Description: Evalúa acceso SSH y privilegios sudo para cada host del inventario +# Author: Luis GuLo +# Version: 0.4.0 + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +INVENTORY="$PROJECT_ROOT/core/inventory/hosts.yaml" +REPORT="$PROJECT_ROOT/core/inventory/trust_report.yaml" +TIMEOUT=5 +USER="${USER:-$(whoami)}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +export SHFLOW_LANG="${SHFLOW_LANG:-es}" +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" + +trfile="$PROJECT_ROOT/core/utils/shflow-trust.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +echo "$(render_msg "${tr[start]}" "user=$USER")" +echo "$(render_msg "${tr[inventory]}" "path=$INVENTORY")" +echo "$(render_msg "${tr[report]}" "path=$REPORT")" +echo "" + +# 🧪 Validar dependencia yq +if ! command -v yq &>/dev/null; then + echo "$(render_msg "${tr[missing_dep]}" "cmd=yq")" + exit 1 +fi + +# 🧹 Regenerar informe +{ + echo "# $(render_msg "${tr[report_title]}")" + echo "# $(render_msg "${tr[report_date]}" "date=$(date)")" + echo "" +} > "$REPORT" + +# 🔁 Extraer hosts +HOSTS=() +HOSTS_RAW=$(yq ".all.hosts | keys | .[]" "$INVENTORY") +[ -z "$HOSTS_RAW" ] && echo "${tr[no_hosts]:-❌ No se encontraron hosts en el inventario.}" && exit 1 + +while IFS= read -r line; do + HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')") # Eliminar comillas +done <<< "$HOSTS_RAW" + +# 🔍 Evaluar cada host +for host in "${HOSTS[@]}"; do + IP=$(yq -r ".all.hosts.\"$host\".ansible_host" "$INVENTORY") + [[ "$IP" == "null" || -z "$IP" ]] && echo "$(render_msg "${tr[missing_ip]}" "host=$host")" && continue + + echo "$(render_msg "${tr[checking]}" "host=$host" "ip=$IP")" + + if ssh -o BatchMode=yes -o ConnectTimeout=$TIMEOUT "$USER@$IP" 'echo ok' &>/dev/null; then + echo "${tr[ssh_ok]:- ✅ SSH: ok}" + SSH_STATUS="ok" + + if ssh -o BatchMode=yes "$USER@$IP" 'sudo -n true' &>/dev/null; then + echo "${tr[sudo_ok]:- ✅ SUDO: ok}" + SUDO_STATUS="ok" + else + echo "${tr[sudo_pw]:- ⚠️ SUDO: requiere contraseña o no permitido}" + SUDO_STATUS="password_required" + fi + else + echo "${tr[ssh_fail]:- ❌ SSH: fallo de conexión}" + SSH_STATUS="failed" + SUDO_STATUS="unknown" + fi + + { + echo "$host:" + echo " ip: $IP" + echo " ssh: $SSH_STATUS" + echo " sudo: $SUDO_STATUS" + echo "" + } >> "$REPORT" +done + +echo "$(render_msg "${tr[done]}" "path=$REPORT")" diff --git a/core/utils/shflow-trust.tr.en b/core/utils/shflow-trust.tr.en new file mode 100644 index 0000000..5e1209f --- /dev/null +++ b/core/utils/shflow-trust.tr.en @@ -0,0 +1,14 @@ +start=🔍 Evaluating SSH and sudo trust for user: {user} +inventory=📁 Inventory: {path} +report=📄 Report: {path} +missing_dep=❌ Requires '{cmd}' (Go version) to process YAML inventory +report_title=Trust report generated by shflow-trust +report_date=Date: {date} +no_hosts=❌ No hosts found in inventory. +missing_ip=⚠️ Host '{host}' has no ansible_host defined +checking=🖥️ Host: {host} ({ip}) +ssh_ok= ✅ SSH: ok +ssh_fail= ❌ SSH: connection failed +sudo_ok= ✅ SUDO: ok +sudo_pw= ⚠️ SUDO: requires password or not allowed +done=✅ Report completed: {path} diff --git a/core/utils/shflow-trust.tr.es b/core/utils/shflow-trust.tr.es new file mode 100644 index 0000000..9e930a9 --- /dev/null +++ b/core/utils/shflow-trust.tr.es @@ -0,0 +1,14 @@ +start=🔍 Evaluando confianza SSH y sudo para usuario: {user} +inventory=📁 Inventario: {path} +report=📄 Informe: {path} +missing_dep=❌ Requiere '{cmd}' versión Go para procesar el inventario YAML +report_title=Informe de confianza generado por shflow-trust +report_date=Fecha: {date} +no_hosts=❌ No se encontraron hosts en el inventario. +missing_ip=⚠️ Host '{host}' sin ansible_host definido +checking=🖥️ Host: {host} ({ip}) +ssh_ok= ✅ SSH: ok +ssh_fail= ❌ SSH: fallo de conexión +sudo_ok= ✅ SUDO: ok +sudo_pw= ⚠️ SUDO: requiere contraseña o no permitido +done=✅ Informe completado: {path} diff --git a/core/utils/vault-init.sh b/core/utils/vault-init.sh new file mode 100755 index 0000000..3055a10 --- /dev/null +++ b/core/utils/vault-init.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# ShFlow Vault Initializer +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.3.0 +# Dependencies: gpg + +set -euo pipefail + +# 📁 Rutas defensivas +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +VAULT_DIR="$PROJECT_ROOT/core/vault" +VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}" +VAULT_PUBKEY="${VAULT_PUBKEY:-$HOME/.shflow.pub}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/core/utils/vault-init.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +generate_key() { + echo "${tr[gen_key]:-🔐 Generando nueva clave simétrica...}" + head -c 64 /dev/urandom | base64 > "$VAULT_KEY" + chmod 600 "$VAULT_KEY" + echo "$(render_msg "${tr[key_created]}" "path=$VAULT_KEY")" +} + +rotate_key() { + echo "${tr[rotate_start]:-🔄 Rotando clave y re-cifrando secretos...}" + local OLD_KEY="$VAULT_KEY.old" + + cp "$VAULT_KEY" "$OLD_KEY" + generate_key + + for file in "$VAULT_DIR"/*.gpg; do + key=$(basename "$file" .gpg) + echo "$(render_msg "${tr[recrypt]}" "key=$key")" + gpg --quiet --batch --yes --passphrase-file "$OLD_KEY" -d "$file" | \ + gpg --symmetric --batch --yes --passphrase-file "$VAULT_KEY" -o "$VAULT_DIR/$key.gpg.new" + mv "$VAULT_DIR/$key.gpg.new" "$VAULT_DIR/$key.gpg" + done + + echo "$(render_msg "${tr[rotate_done]}" "path=$OLD_KEY")" +} + +status() { + echo "${tr[status_title]:-📊 Estado del Vault}" + echo "-------------------" + echo "$(render_msg "${tr[sym_key]}" "status=$( [ -f "$VAULT_KEY" ] && echo "${tr[present]}" || echo "${tr[absent]}")")" + echo "$(render_msg "${tr[pub_key]}" "status=$( [ -f "$VAULT_PUBKEY" ] && echo "${tr[present]}" || echo "${tr[absent]}")")" + echo "$(render_msg "${tr[vault_path]}" "path=$VAULT_DIR")" + echo "$(render_msg "${tr[secrets]}" "count=$(ls "$VAULT_DIR"/*.gpg 2>/dev/null | wc -l)")" + echo "$(render_msg "${tr[last_mod]}" "date=$(date -r "$VAULT_KEY" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)")" +} + +generate_pubkey() { + echo "${tr[asym_start]:-🔐 Configurando cifrado asimétrico...}" + echo "${tr[asym_hint]:-⚠️ Se requiere que la clave pública esté exportada previamente.}" + echo " gpg --export -a 'usuario@dominio' > $VAULT_PUBKEY" + if [ -f "$VAULT_PUBKEY" ]; then + echo "$(render_msg "${tr[pubkey_found]}" "path=$VAULT_PUBKEY")" + else + echo "${tr[pubkey_missing]:-❌ Clave pública no encontrada. Exporta primero con GPG.}" + exit 1 + fi +} + +main() { + case "${1:-}" in + --rotate) + [ -f "$VAULT_KEY" ] || { echo "${tr[no_key]:-❌ No existe clave actual. Ejecuta sin --rotate primero.}"; exit 1; } + rotate_key + ;; + --status) + status + ;; + --asymmetric) + generate_pubkey + ;; + *) + if [ -f "$VAULT_KEY" ]; then + echo "$(render_msg "${tr[key_exists]}" "path=$VAULT_KEY")" + else + generate_key + fi + ;; + esac +} + +main "$@" diff --git a/core/utils/vault-init.tr.en b/core/utils/vault-init.tr.en new file mode 100644 index 0000000..8004348 --- /dev/null +++ b/core/utils/vault-init.tr.en @@ -0,0 +1,19 @@ +gen_key=🔐 Generating new symmetric key... +key_created=✅ Key created at {path} +rotate_start=🔄 Rotating key and re-encrypting secrets... +recrypt=🔁 Re-encrypting '{key}'... +rotate_done=✅ Rotation completed. Old key saved at {path} +status_title=📊 Vault Status +sym_key=🔐 Symmetric key: {status} +pub_key=🔐 Public key: {status} +present=✅ present +absent=❌ absent +vault_path=📁 Vault path: {path} +secrets=📦 Secrets: {count} +last_mod=🕒 Last modified: {date} +asym_start=🔐 Setting up asymmetric encryption... +asym_hint=⚠️ Public key must be exported beforehand. +pubkey_found=✅ Public key found at {path} +pubkey_missing=❌ Public key not found. Export it first using GPG. +no_key=❌ No current key found. Run without --rotate first. +key_exists=🔐 Key already exists at {path} diff --git a/core/utils/vault-init.tr.es b/core/utils/vault-init.tr.es new file mode 100644 index 0000000..8ed9644 --- /dev/null +++ b/core/utils/vault-init.tr.es @@ -0,0 +1,19 @@ +gen_key=🔐 Generando nueva clave simétrica... +key_created=✅ Clave creada en {path} +rotate_start=🔄 Rotando clave y re-cifrando secretos... +recrypt=🔁 Re-cifrando '{key}'... +rotate_done=✅ Rotación completada. Clave antigua guardada en {path} +status_title=📊 Estado del Vault +sym_key=🔐 Clave simétrica: {status} +pub_key=🔐 Clave pública: {status} +present=✅ presente +absent=❌ ausente +vault_path=📁 Ruta del vault: {path} +secrets=📦 Secretos: {count} +last_mod=🕒 Última modificación: {date} +asym_start=🔐 Configurando cifrado asimétrico... +asym_hint=⚠️ Se requiere que la clave pública esté exportada previamente. +pubkey_found=✅ Clave pública detectada en {path} +pubkey_missing=❌ Clave pública no encontrada. Exporta primero con GPG. +no_key=❌ No existe clave actual. Ejecuta sin --rotate primero. +key_exists=🔐 Clave ya existe en {path} diff --git a/core/utils/vault_utils.sh b/core/utils/vault_utils.sh new file mode 100755 index 0000000..702265f --- /dev/null +++ b/core/utils/vault_utils.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Utility: vault_utils +# Description: Funciones para acceso seguro al vault de ShFlow +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.1.0 +# Dependencies: gpg + +VAULT_DIR="${VAULT_DIR:-core/vault}" +VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}" + +# 🧩 Cargar render_msg si no está disponible +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/core/utils/vault_utils.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +get_secret() { + local key="$1" + local value + + if [ ! -f "$VAULT_DIR/$key.gpg" ]; then + echo "$(render_msg "${tr[missing]}" "key=$key" "dir=$VAULT_DIR")" + return 1 + fi + + value=$(gpg --quiet --batch --yes --passphrase-file "$VAULT_KEY" -d "$VAULT_DIR/$key.gpg" 2>/dev/null) + if [ $? -ne 0 ]; then + echo "$(render_msg "${tr[decrypt_fail]}" "key=$key")" + return 1 + fi + + echo "$value" +} diff --git a/core/utils/vault_utils.tr.en b/core/utils/vault_utils.tr.en new file mode 100644 index 0000000..413952e --- /dev/null +++ b/core/utils/vault_utils.tr.en @@ -0,0 +1,2 @@ +missing=❌ [vault] Secret '{key}' not found in {dir} +decrypt_fail=❌ [vault] Failed to decrypt '{key}' diff --git a/core/utils/vault_utils.tr.es b/core/utils/vault_utils.tr.es new file mode 100644 index 0000000..f81ee15 --- /dev/null +++ b/core/utils/vault_utils.tr.es @@ -0,0 +1,2 @@ +missing=❌ [vault] Secreto '{key}' no encontrado en {dir} +decrypt_fail=❌ [vault] Error al descifrar '{key}' diff --git a/core/vault/.gitignore b/core/vault/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/examples/api.yaml b/examples/api.yaml new file mode 100644 index 0000000..057dd22 --- /dev/null +++ b/examples/api.yaml @@ -0,0 +1,9 @@ +tasks: + - name: Crear producto de prueba + module: api + args: + method: post + url: https://fakestoreapi.com/products + body: '{"title":"Producto ShFlow","price":99.99,"description":"Generado desde módulo api.sh","image":"https://example.com/img.png","category":"electronics"}' + parse: json + diff --git a/examples/api_body_test.yaml b/examples/api_body_test.yaml new file mode 100644 index 0000000..fe0578f --- /dev/null +++ b/examples/api_body_test.yaml @@ -0,0 +1,62 @@ +tasks: + - name: POST simple con campos planos + module: api + args: + method: post + url: https://reqres.in/api/users + body: | + {"name":"Luis","job":"ShFlow tester"} + headers: x-api-key:reqres-free-v1,Content-Type:application/json + parse: json + + - name: POST con estructura anidada + module: api + args: + method: post + url: https://httpbin.org/post + body: | + { + "title": "Test", + "tags": ["bash", "api", "flow"], + "meta": { + "author": "Luis", + "version": "1.0" + } + } + headers: Content-Type:application/json + parse: json + + - name: POST con cuerpo vacío + module: api + args: + method: post + url: https://httpbin.org/post + body: "" + headers: Content-Type:application/json + parse: json + + - name: POST con caracteres especiales + module: api + args: + method: post + url: https://httpbin.org/post + body: | + {"mensaje":"¡Hola desde ShFlow! 🎉 Ñandú con tilde y ñ"} + headers: Content-Type:application/json + parse: json + + - name: POST con array de objetos + module: api + args: + method: post + url: https://httpbin.org/post + body: | + { + "usuarios": [ + {"nombre": "Luis", "rol": "admin"}, + {"nombre": "Ana", "rol": "tester"} + ] + } + headers: Content-Type:application/json + parse: json + diff --git a/examples/api_get-json.yaml b/examples/api_get-json.yaml new file mode 100644 index 0000000..eb3619c --- /dev/null +++ b/examples/api_get-json.yaml @@ -0,0 +1,7 @@ +tasks: + - name: Obtener producto fake + module: api + args: + method: get + url: https://fakestoreapi.com/products/1 + parse: json diff --git a/examples/api_post-json.yaml b/examples/api_post-json.yaml new file mode 100644 index 0000000..20710dd --- /dev/null +++ b/examples/api_post-json.yaml @@ -0,0 +1,8 @@ +- name: Crear usuario + module: api + args: + method: post + url: https://api.example.com/users + headers: Content-Type:application/json + body: '{"name":"Luis","role":"admin"}' + parse: json diff --git a/examples/api_post.yaml b/examples/api_post.yaml new file mode 100644 index 0000000..87c640d --- /dev/null +++ b/examples/api_post.yaml @@ -0,0 +1,10 @@ +tasks: + - name: Crear producto de prueba + module: api + args: + method: post + url: https://fakestoreapi.com/products +body: | + {"title":"Producto ShFlow","price":99.99,"description":"Generado desde módulo api.sh","image":"https://example.com/img.png","category":"electronics"} + parse: json + diff --git a/examples/api_reqres.yaml b/examples/api_reqres.yaml new file mode 100644 index 0000000..d261ff8 --- /dev/null +++ b/examples/api_reqres.yaml @@ -0,0 +1,11 @@ +tasks: + - name: Crear usuario de prueba + module: api + args: + method: post + url: https://reqres.in/api/users + body: | + {"name":"Luis","job":"ShFlow tester"} + headers: x-api-key:reqres-free-v1,Content-Type:application/json + parse: json + diff --git a/examples/api_soap-xml.yaml b/examples/api_soap-xml.yaml new file mode 100644 index 0000000..7517a65 --- /dev/null +++ b/examples/api_soap-xml.yaml @@ -0,0 +1,14 @@ +tasks: + - name: Consulta SOAP de prueba + module: api + args: + method: soap + url: http://demo1144442.mockable.io/ + headers: Content-Type:text/xml + body: | + + + + + + parse: xml diff --git a/examples/apt-update.yaml b/examples/apt-update.yaml new file mode 100644 index 0000000..7663dcd --- /dev/null +++ b/examples/apt-update.yaml @@ -0,0 +1,13 @@ +tasks: + - name: Actualizar datos del Repo + module: run + args: + command: "apt-get -y update" + become: true + + - name: Actualizar SO + module: run + args: + command: "apt-get -y upgrade" + become: true + diff --git a/examples/archive-compress-tar.gz.yaml b/examples/archive-compress-tar.gz.yaml new file mode 100644 index 0000000..c52ab8c --- /dev/null +++ b/examples/archive-compress-tar.gz.yaml @@ -0,0 +1,9 @@ +tasks: + - name: Comprimir logs + module: archive + args: + action: compress + format: tar + files: /var/log/boot.log,/var/log/lastlog + output: /tmp/logs.tar.gz + become: true diff --git a/examples/archive-decompress-zip.yaml b/examples/archive-decompress-zip.yaml new file mode 100644 index 0000000..fa6d436 --- /dev/null +++ b/examples/archive-decompress-zip.yaml @@ -0,0 +1,8 @@ +tasks: + - name: Descomprimir ZIP + module: archive + args: + action: decompress + format: zip + archive: /tmp/project.zip + dest: /tmp/destino/project diff --git a/examples/archive-extract-bootlog.yaml b/examples/archive-extract-bootlog.yaml new file mode 100644 index 0000000..7816a86 --- /dev/null +++ b/examples/archive-extract-bootlog.yaml @@ -0,0 +1,11 @@ +tasks: + - name: Extraer solo boot.log desde logs.tar.gz + module: archive + args: + action: extract + format: tar + archive: /tmp/logs.tar.gz + dest: /tmp/extraccion/ + files: var/log/boot.log + become: true + diff --git a/examples/archive-extract-tar.gz.yaml b/examples/archive-extract-tar.gz.yaml new file mode 100644 index 0000000..3e8863b --- /dev/null +++ b/examples/archive-extract-tar.gz.yaml @@ -0,0 +1,9 @@ +tasks: + - name: Extraer backup + module: archive + args: + action: extract + format: tar + archive: /tmp/logs.tar.gz + dest: /tmp/restore + become: true diff --git a/examples/basic_conditions.yaml b/examples/basic_conditions.yaml new file mode 100644 index 0000000..655b800 --- /dev/null +++ b/examples/basic_conditions.yaml @@ -0,0 +1,15 @@ +hosts: localhost +parallelism: false + +tasks: + - name: Pausa si el host es localhost + module: wait + args: + seconds: 1 + condition: '[ "{{ name }}" = "localhost" ]' + + - name: Pausa solo si existe el archivo /tmp/flag.txt + module: wait + args: + seconds: 2 + condition: 'test -f /tmp/flag.txt' diff --git a/examples/block-in-file.yaml b/examples/block-in-file.yaml new file mode 100644 index 0000000..2cdd7eb --- /dev/null +++ b/examples/block-in-file.yaml @@ -0,0 +1,10 @@ +- name: Añadir bloque de configuración a NGINX + module: blockinfile + args: + path: /etc/nginx/nginx.conf + block: | + server_tokens off; + keepalive_timeout 65; + marker: NGINX_CONF + backup: true + become: true diff --git a/examples/clone-and-configure.yaml b/examples/clone-and-configure.yaml new file mode 100644 index 0000000..5de2669 --- /dev/null +++ b/examples/clone-and-configure.yaml @@ -0,0 +1,24 @@ +tasks: + - name: Clonar repo de configuración + module: git + args: + action: clone + repo: "https://github.com/luisgulo/configs.git" + dest: "/opt/configs" + become: true + + - name: Copiar archivo de configuración + module: git + args: + action: fetch-file + repo: "https://github.com/luisgulo/configs.git" + branch: "main" + file_path: "nginx/nginx.conf" + dest: "/etc/nginx/nginx.conf" + become: true + + - name: Reiniciar nginx + module: run + args: + command: "systemctl restart nginx" + become: true diff --git a/examples/cron_root.yaml b/examples/cron_root.yaml new file mode 100644 index 0000000..e54f00f --- /dev/null +++ b/examples/cron_root.yaml @@ -0,0 +1,46 @@ +hosts: localhost +parallelism: false + +tasks: + - name: Crear cron ver_df cada hora + module: cron + args: + alias: ver_df + user: root + state: present + schedule: "0 * * * *" + command: "df -h > /tmp/filesize.txt" + + - name: Listar cron de root (tras creación) + module: cron + args: + user: root + state: list + + - name: Modificar cron ver_df a cada 3 horas + module: cron + args: + alias: ver_df + user: root + state: present + schedule: "0 */3 * * *" + command: "df -h > /tmp/filesize.txt" + + - name: Listar cron de root (tras modificación) + module: cron + args: + user: root + state: list + + - name: Eliminar cron ver_df + module: cron + args: + alias: ver_df + user: root + state: absent + + - name: Listar cron de root (tras eliminación) + module: cron + args: + user: root + state: list diff --git a/examples/deploy-nginx.yaml b/examples/deploy-nginx.yaml new file mode 100644 index 0000000..d2c9c31 --- /dev/null +++ b/examples/deploy-nginx.yaml @@ -0,0 +1,24 @@ +tasks: + - name: Crear directorio web + module: file + args: + path: "/var/www/html" + state: present + type: directory + mode: "0755" + become: true + + - name: Ejecutar contenedor nginx + module: docker + args: + action: present + name: nginx_web + image: nginx:latest + become: true + + - name: Habilitar servicio docker + module: service + args: + name: docker + state: enable + become: true diff --git a/examples/docker_and_ldapsearch.yaml b/examples/docker_and_ldapsearch.yaml new file mode 100644 index 0000000..50cf48e --- /dev/null +++ b/examples/docker_and_ldapsearch.yaml @@ -0,0 +1,31 @@ +tasks: + - name: Lanzar contenedor LDAP + module: docker + args: + action: present + name: test-ldap + image: osixia/openldap:latest + run_args: "-p 389:389 -p 636:636 -e LDAP_ORGANISATION='Test Org' -e LDAP_DOMAIN='test.local' -e LDAP_ADMIN_PASSWORD='secreta'" + + - name: Esperar 5 segundos para que LDAP esté listo + module: wait + args: + seconds: 5 + + - name: Buscar usuarios en OpenLDAP + module: ldap_openldap + args: + state: search + server: ldap://localhost + port: 389 + base_dn: "dc=test,dc=local" + filter: "(objectClass=*)" + attributes: "cn,mail,uid" + bind_dn: "cn=admin,dc=test,dc=local" + password: "secreta" + + #- name: Eliminar LDAP + # module: docker + # args: + # action: absent + # name: test-ldap diff --git a/examples/docker_build-shflow.yaml b/examples/docker_build-shflow.yaml new file mode 100644 index 0000000..f72ebd3 --- /dev/null +++ b/examples/docker_build-shflow.yaml @@ -0,0 +1,8 @@ +tasks: + - name: Construir imagen test-shflow desde debian:trixie + module: docker + args: + action: build + image: test-shflow + path: /tmp/shflow-build/ + become: true diff --git a/examples/docker_hello-world.yaml b/examples/docker_hello-world.yaml new file mode 100644 index 0000000..cabc9d6 --- /dev/null +++ b/examples/docker_hello-world.yaml @@ -0,0 +1,8 @@ +tasks: + - name: Probar imagen hello-world + module: docker + args: + action: present + name: test-hello + image: hello-world + detach: false diff --git a/examples/download.yaml b/examples/download.yaml new file mode 100644 index 0000000..ce1276e --- /dev/null +++ b/examples/download.yaml @@ -0,0 +1,9 @@ +tasks: + - name: Descargar binario de servicio + module: download + args: + url: https://example.com/binario.tar.gz + dest: /tmp/manual.pdf + proxy: http://proxy.local:3128 + continue: true + become: true diff --git a/examples/echo_capture.yaml b/examples/echo_capture.yaml new file mode 100644 index 0000000..fa44030 --- /dev/null +++ b/examples/echo_capture.yaml @@ -0,0 +1,32 @@ +parallelism: false +hosts: localhost + +tasks: + - name: Ejecutar comando simple + module: run + args: + command: "ls -L /tmpAAA" + become: false + capture_log: ls_resultado + capture_err: ls_error + + - name: Mostrar capture_log con modulo echo + module: echo + args: + message: "{{ ls_resultado }}" + become: false + condition: "[ {{ ls_error }} -eq 0 ]" + + - name: Mostrar capture_err con modulo echo + module: echo + args: + message: "{{ ls_error }}" + become: false + + - name: Prueba sustitucion en modulo echo + module: echo + args: + variableA: "Texto de variable A" + variableB: "Texto de variable B" + message: "Mostramos A: {{ variableA }} y B: {{ variableB }}" + become: false diff --git a/examples/euvd_check.yaml b/examples/euvd_check.yaml new file mode 100644 index 0000000..5a52dc4 --- /dev/null +++ b/examples/euvd_check.yaml @@ -0,0 +1,17 @@ +hosts: servidor_pruebas +tasks: + - name: Verificar vulnerabilidad openssl EUVD + module: euvd_check + args: + state: check + enisa_id: EUVD-2025-31120 + package: openssl + become: true + + - name: Verificar vulnerabilidad sudo EUVD + module: euvd_check + args: + state: check + enisa_id: EUVD-2025-19673 + package: sudo + become: true diff --git a/examples/facts_key-value.yaml b/examples/facts_key-value.yaml new file mode 100644 index 0000000..8e18977 --- /dev/null +++ b/examples/facts_key-value.yaml @@ -0,0 +1,6 @@ +tasks: + - name: IPs del host + module: facts + args: + field: ip_addresses + format: kv diff --git a/examples/facts_markdown.yaml b/examples/facts_markdown.yaml new file mode 100644 index 0000000..83bd82f --- /dev/null +++ b/examples/facts_markdown.yaml @@ -0,0 +1,8 @@ +tasks: + - name: Inventario de servidores + module: facts + args: + format: md + output: /tmp/informe.md + append: true + host_label: servidor-01 diff --git a/examples/file_read_apache-config.yaml b/examples/file_read_apache-config.yaml new file mode 100644 index 0000000..d402c51 --- /dev/null +++ b/examples/file_read_apache-config.yaml @@ -0,0 +1,10 @@ +hosts: localhost +parallelism: false + +tasks: + - name: Leer configuración de Apache sin comentarios + module: file_read + args: + path: "/etc/apache2/apache2.conf" + grep: "^[^#]" + become: true diff --git a/examples/fs-copy.yaml b/examples/fs-copy.yaml new file mode 100644 index 0000000..83dffbd --- /dev/null +++ b/examples/fs-copy.yaml @@ -0,0 +1,6 @@ +- name: Copiar config + module: fs + args: + action: copy + src: /etc/nginx/nginx.conf + dest: /tmp/nginx.conf.bak diff --git a/examples/fs-delete-multiple.yaml b/examples/fs-delete-multiple.yaml new file mode 100644 index 0000000..37d90b7 --- /dev/null +++ b/examples/fs-delete-multiple.yaml @@ -0,0 +1,5 @@ +- name: Eliminar temporales + module: fs + args: + action: delete + files: /tmp/debug.log,/tmp/test.out diff --git a/examples/fs-delete.yaml b/examples/fs-delete.yaml new file mode 100644 index 0000000..48bc035 --- /dev/null +++ b/examples/fs-delete.yaml @@ -0,0 +1,5 @@ +- name: Eliminar temporal + module: fs + args: + action: delete + path: /tmp/debug.log diff --git a/examples/fs-move-multiple.yaml b/examples/fs-move-multiple.yaml new file mode 100644 index 0000000..45c09f0 --- /dev/null +++ b/examples/fs-move-multiple.yaml @@ -0,0 +1,6 @@ +- name: Mover logs antiguos + module: fs + args: + action: move + files: /var/log/app1.log,/var/log/app2.log + dest: /tmp/logs diff --git a/examples/fs-move.yaml b/examples/fs-move.yaml new file mode 100644 index 0000000..34144dd --- /dev/null +++ b/examples/fs-move.yaml @@ -0,0 +1,6 @@ +- name: Mover log + module: fs + args: + action: move + src: /var/log/app.log + dest: /tmp/app.log diff --git a/examples/fs-rename.yaml b/examples/fs-rename.yaml new file mode 100644 index 0000000..ef5eb20 --- /dev/null +++ b/examples/fs-rename.yaml @@ -0,0 +1,6 @@ +- name: Renombrar backup + module: fs + args: + action: rename + src: /home/user/backup.tar.gz + dest: /home/user/backup_old.tar.gz diff --git a/examples/fs-truncate-multiple.yaml b/examples/fs-truncate-multiple.yaml new file mode 100644 index 0000000..033418f --- /dev/null +++ b/examples/fs-truncate-multiple.yaml @@ -0,0 +1,5 @@ +- name: Vaciar logs + module: fs + args: + action: truncate + files: /var/log/app1.log,/var/log/app2.log diff --git a/examples/fs-truncate.yaml b/examples/fs-truncate.yaml new file mode 100644 index 0000000..986b8b9 --- /dev/null +++ b/examples/fs-truncate.yaml @@ -0,0 +1,5 @@ +- name: Vaciar log + module: fs + args: + action: truncate + path: /var/log/app.log diff --git a/examples/full-stack.yaml b/examples/full-stack.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/groups_minimal.yaml b/examples/groups_minimal.yaml new file mode 100644 index 0000000..882ad61 --- /dev/null +++ b/examples/groups_minimal.yaml @@ -0,0 +1,6 @@ +tasks: + - name: Crear grupo de servicio + module: groups + args: + groupname: servicio + become: true diff --git a/examples/install-tools.yaml b/examples/install-tools.yaml new file mode 100644 index 0000000..98d4ecf --- /dev/null +++ b/examples/install-tools.yaml @@ -0,0 +1,14 @@ +tasks: + - name: Instalar curl + module: package + args: + name: curl + state: present + become: true + + - name: Instalar vim + module: package + args: + name: vim + state: present + become: true diff --git a/examples/ldap_search.yaml b/examples/ldap_search.yaml new file mode 100644 index 0000000..11d3bab --- /dev/null +++ b/examples/ldap_search.yaml @@ -0,0 +1,24 @@ +tasks: + - name: Buscar usuarios en OpenLDAP + module: ldap_openldap + args: + state: search + server: ldap://ldap.example.com + port: 389 + base_dn: "dc=example,dc=com" + filter: "(objectClass=person)" + attributes: "cn,mail,uid" + bind_dn: "cn=admin,dc=example,dc=com" + password: "secreta" + + - name: Buscar usuarios en Active Directory + module: ldap_ad + args: + state: search + server: ldap://ad.example.local + port: 389 + base_dn: "dc=example,dc=local" + filter: "(sAMAccountName=luisgulo)" + attributes: "cn,mail,sAMAccountName" + bind_dn: "CN=Administrador,CN=Users,DC=example,DC=local" + password: "secreta" diff --git a/examples/line-in-file.yaml b/examples/line-in-file.yaml new file mode 100644 index 0000000..3a874ba --- /dev/null +++ b/examples/line-in-file.yaml @@ -0,0 +1,8 @@ +- name: Asegurar configuración de NGINX + module: lineinfile + args: + path: /etc/nginx/nginx.conf + line: 'client_max_body_size 20M;' + regexp: '^client_max_body_size' + backup: true + become: true diff --git a/examples/log-shflow.yaml b/examples/log-shflow.yaml new file mode 100644 index 0000000..0166271 --- /dev/null +++ b/examples/log-shflow.yaml @@ -0,0 +1,15 @@ +tasks: + - name: Crear directorio temporal + module: file + args: + path: "/tmp/shflow" + state: present + type: directory + mode: "0755" + become: true + + - name: Mostrar fecha en remoto + module: run + args: + command: "date" + become: false diff --git a/examples/lookup_password.yaml b/examples/lookup_password.yaml new file mode 100644 index 0000000..eb5e3b8 --- /dev/null +++ b/examples/lookup_password.yaml @@ -0,0 +1,18 @@ +hosts: localhost +parallelism: false + +# Nota: Crear clave con: shflow-vault add smtp_pass +vars: + clave_email: "{{ vault('smtp_pass') }}" + +tasks: + - name: Mostrar clave recogida del vault + module: echo + args: + message: "🔐 Secreto descifrado: {{ clave_email }}" + + - name: Mostrar valor directamente desde el vault + module: echo + args: + message: "Valor directo del vault: {{ vault('smtp_pass') }}" + diff --git a/examples/loop.yaml b/examples/loop.yaml new file mode 100644 index 0000000..474eb38 --- /dev/null +++ b/examples/loop.yaml @@ -0,0 +1,9 @@ +- name: Crear usuarios + module: loop + args: + items: alice,bob,carol + module: useradd + args: + name: {{item}} + shell: /bin/bash + become: true diff --git a/examples/loop_cartesian-values.yaml b/examples/loop_cartesian-values.yaml new file mode 100644 index 0000000..bfb2c4b --- /dev/null +++ b/examples/loop_cartesian-values.yaml @@ -0,0 +1,9 @@ +- name: Asignar permisos cruzados + module: loop + args: + items: alice,bob + secondary: read,write + module: permission + args: + user: {{item}} + mode: {{secondary_item}} diff --git a/examples/loop_fail-fast.yaml b/examples/loop_fail-fast.yaml new file mode 100644 index 0000000..200f1ca --- /dev/null +++ b/examples/loop_fail-fast.yaml @@ -0,0 +1,10 @@ +- name: Crear usuarios sin detenerse ante errores + module: loop + args: + items: alice,bob,carol + module: useradd + fail_fast: false + args: + name: {{item}} + shell: /bin/bash + become: true diff --git a/examples/loop_key-value.yaml b/examples/loop_key-value.yaml new file mode 100644 index 0000000..d1d54de --- /dev/null +++ b/examples/loop_key-value.yaml @@ -0,0 +1,10 @@ +- name: Asignar usuarios a grupos + module: loop + args: + items: alice:sudo,bob:docker,carol:admin + module: useradd + args: + name: {{item_key}} + groups: {{item_value}} + shell: /bin/bash + become: true diff --git a/examples/loop_matrix.yaml b/examples/loop_matrix.yaml new file mode 100644 index 0000000..35253db --- /dev/null +++ b/examples/loop_matrix.yaml @@ -0,0 +1,14 @@ +hosts: localhost +parallelism: false + +tasks: + - name: Probar conectividad entre múltiples orígenes y destinos + module: loop + args: + items: "equipo1:192.168.1.1,equipo2:10.0.0.1" + secondary: "8.8.8.8,1.1.1.1" + module: ping + count: 2 + timeout: 3 + target: "{{secondary_item}}" + become: true diff --git a/examples/ls.yaml b/examples/ls.yaml new file mode 100644 index 0000000..e9ba61e --- /dev/null +++ b/examples/ls.yaml @@ -0,0 +1,13 @@ +tasks: + - name: Listar directorios tmp + module: run + args: + command: "ls -l /tmp" + become: false + + - name: Listar directorios /root + module: run + args: + command: "ls -l /root" + become: true + diff --git a/examples/minimal.yaml b/examples/minimal.yaml new file mode 100644 index 0000000..fb04e0f --- /dev/null +++ b/examples/minimal.yaml @@ -0,0 +1,15 @@ +tasks: + - name: Crear directorio de logs + module: file + args: + path: "/var/log/shflow" + state: present + type: directory + mode: "0755" + become: true + + - name: Mostrar fecha en remoto + module: run + args: + command: "date" + become: false diff --git a/examples/openssl_autogen.yaml b/examples/openssl_autogen.yaml new file mode 100644 index 0000000..8c02e70 --- /dev/null +++ b/examples/openssl_autogen.yaml @@ -0,0 +1,46 @@ +hosts: localhost +parallelism: false + +tasks: + - name: Generar certificado autofirmado en /tmp + module: run + args: + command: | + openssl req -x509 -newkey rsa:2048 -keyout /tmp/test.key -out /tmp/test.crt \ + -days 365 -nodes -subj "/CN=example.com" && \ + openssl pkcs12 -export -out /tmp/certificado.pfx \ + -inkey /tmp/test.key -in /tmp/test.crt \ + -name "Certificado de Prueba" -password pass:secreta + become: false + + - name: Convertir PFX a PEM + module: openssl + args: + state: convert + src: /tmp/certificado.pfx + dest: /tmp/certificado.pem + format: pem + password: "secreta" + + - name: Inspeccionar certificado convertido + module: openssl + args: + state: inspect + src: /tmp/certificado.pem + + - name: Instalar certificado como CA confiable + module: openssl + args: + state: trust + src: /tmp/certificado.pem + alias: mi_certificado + trust_path: /usr/local/share/ca-certificates/ + become: true + + - name: Eliminar certificado como CA + module: openssl + args: + state: untrust + alias: mi_certificado + trust_path: /usr/local/share/ca-certificates/ + become: true diff --git a/examples/ping.yaml b/examples/ping.yaml new file mode 100644 index 0000000..b541256 --- /dev/null +++ b/examples/ping.yaml @@ -0,0 +1,7 @@ +tasks: + - name: Verificar conectividad con servidor + module: ping + args: + count: 3 + timeout: 5 + become: false diff --git a/examples/remote-ping.yaml b/examples/remote-ping.yaml new file mode 100644 index 0000000..075c628 --- /dev/null +++ b/examples/remote-ping.yaml @@ -0,0 +1,8 @@ +tasks: + - name: Verificar acceso a gateway desde web01 + module: ping + args: + target: 192.168.1.12 + count: 4 + timeout: 5 + become: true diff --git a/examples/remote_user.yaml b/examples/remote_user.yaml new file mode 100644 index 0000000..ffda478 --- /dev/null +++ b/examples/remote_user.yaml @@ -0,0 +1,9 @@ +vars: + remote_user: other + +tasks: + - name: listar directorio de root desde el usuario "other" que tiene sudo + module: run + args: + command: "ls -l /root" + become: true diff --git a/examples/replace-string.yaml b/examples/replace-string.yaml new file mode 100644 index 0000000..97f8c9e --- /dev/null +++ b/examples/replace-string.yaml @@ -0,0 +1,8 @@ +- name: Reemplazar puerto en archivo de configuración + module: replace + args: + path: /etc/nginx/sites-available/default + regexp: 'listen\s+80' + replace: 'listen 8080' + backup: true + become: true diff --git a/examples/smtp_send_test.yaml b/examples/smtp_send_test.yaml new file mode 100644 index 0000000..7c350f1 --- /dev/null +++ b/examples/smtp_send_test.yaml @@ -0,0 +1,12 @@ +tasks: + - name: Enviar correo de prueba + module: smtp_send + args: + smtp_server: smtp.example.com + smtp_port: 587 + smtp_user: usuario@example.com + smtp_pass: ClaveSuperSecreta123 + from: usuario@example.com + to: destino@correo.com + subject: Prueba desde ShFlow + body: Este es un correo de prueba enviado desde el módulo smtp_send. diff --git a/examples/template.yaml b/examples/template.yaml new file mode 100644 index 0000000..99d7fb2 --- /dev/null +++ b/examples/template.yaml @@ -0,0 +1,8 @@ +- name: Generar configuración de NGINX + module: template + args: + src: nginx.conf.tmpl + dest: /etc/nginx/nginx.conf + port: 8080 + user: www-data + become: true diff --git a/examples/update_datetime.yaml b/examples/update_datetime.yaml new file mode 100644 index 0000000..3dde6a1 --- /dev/null +++ b/examples/update_datetime.yaml @@ -0,0 +1,43 @@ +parallelism: false + +tasks: + - name: Verificar disponibilidad de chronyc + module: run + args: + command: "command -v chronyc >/dev/null" + capture_err: chrony_err + + - name: Abortar si chronyc no está disponible + module: run + args: + command: "echo '{{ chrony_err }}' | grep -q '1' && echo '❌ Chrony no está instalado. No se puede ajustar la hora.' && exit 1 || true" + + - name: Sincronizar hora con Chrony + module: run + args: + command: "chronyc -a makestep" + become: true + capture_log: sync_result + capture_err: sync_err + + - name: Mostrar valores de sincronizacion + module: echo + args: + message: "Cod.Err: {{ sync_err }} - {{ sync_result }}" + + - name: Validar sincronización + module: run + args: + command: "echo '{{ sync_result }}' | grep -q '200 OK' && echo '✅ Hora sincronizada correctamente' || echo '❌ Fallo en sincronización'" + + + - name: Verificar estado del reloj + module: run + args: + command: "timedatectl status" + + - name: ls en root + module: run + args: + command: "ls -l /root" + become: true diff --git a/examples/update_so.yaml b/examples/update_so.yaml new file mode 100644 index 0000000..5ed8432 --- /dev/null +++ b/examples/update_so.yaml @@ -0,0 +1,10 @@ +parallelism: false + +tasks: + - name: Actualizar sistema completo + module: package + args: + state: system-update + update_type: full + become: true + diff --git a/examples/update_so_security.yaml b/examples/update_so_security.yaml new file mode 100644 index 0000000..09c0054 --- /dev/null +++ b/examples/update_so_security.yaml @@ -0,0 +1,8 @@ +tasks: + - name: Aplicar parches de seguridad (solo RPM) + module: package + args: + state: system-update + update_type: security + become: true + diff --git a/examples/user_create.yaml b/examples/user_create.yaml new file mode 100644 index 0000000..e0bf6be --- /dev/null +++ b/examples/user_create.yaml @@ -0,0 +1,12 @@ +hosts: localhost +parallelism: false +tasks: + - name: Crear usuario de servicio + module: users + args: + state: create + username: servicio + home: /home/servicio + groups: docker + shell: /usr/sbin/nologin + become: true \ No newline at end of file diff --git a/examples/user_delete.yaml b/examples/user_delete.yaml new file mode 100644 index 0000000..db5cc19 --- /dev/null +++ b/examples/user_delete.yaml @@ -0,0 +1,12 @@ +hosts: localhost +parallelism: false +tasks: + - name: Eliminar usuario de servicio + module: users + args: + state: absent + username: servicio + home: /home/servicio + groups: docker + shell: /bin/bash + become: true diff --git a/examples/user_modify.yaml b/examples/user_modify.yaml new file mode 100644 index 0000000..35e629c --- /dev/null +++ b/examples/user_modify.yaml @@ -0,0 +1,12 @@ +hosts: localhost +parallelism: false +tasks: + - name: Modificar shell de servicio + module: users + args: + state: modify + username: servicio + home: /home/servicio + groups: docker + shell: /bin/bash + become: true diff --git a/examples/user_test.yaml b/examples/user_test.yaml new file mode 100644 index 0000000..77d278e --- /dev/null +++ b/examples/user_test.yaml @@ -0,0 +1,32 @@ +hosts: localhost +parallelism: false +tasks: + - name: Crear usuario de servicio + module: users + args: + state: create + username: servicio + home: /home/servicio + groups: docker + shell: /usr/sbin/nologin + become: true + + - name: Modificar shell de servicio + module: users + args: + state: modify + username: servicio + home: /home/servicio + groups: docker + shell: /bin/bash + become: true + + - name: Eliminar usuario de servicio + module: users + args: + state: absent + username: servicio + home: /home/servicio + groups: docker + shell: /bin/bash + become: true diff --git a/examples/useradd.yaml b/examples/useradd.yaml new file mode 100644 index 0000000..0f86ba3 --- /dev/null +++ b/examples/useradd.yaml @@ -0,0 +1,8 @@ +- name: Crear usuario técnico + module: useradd + args: + name: devops + shell: /bin/bash + home: /srv/devops + groups: sudo,docker + become: true diff --git a/examples/vault-demo.yaml b/examples/vault-demo.yaml new file mode 100644 index 0000000..44d3860 --- /dev/null +++ b/examples/vault-demo.yaml @@ -0,0 +1,7 @@ +tasks: + - name: Acceder a API con token seguro + module: run + args: + command: "curl -H \"Authorization: Bearer \$TOKEN\" https://api.example.com/status" + vault_key: "api_token" + become: false diff --git a/examples/vault-sync.yaml b/examples/vault-sync.yaml new file mode 100644 index 0000000..6335379 --- /dev/null +++ b/examples/vault-sync.yaml @@ -0,0 +1,23 @@ +tasks: + - name: Enviar secreto 'api_token' al host remoto + module: vault-remote + args: + action: push + key: api_token + remote_path: "/etc/shflow/vault" + become: true + + - name: Recuperar secreto 'db_password' desde el host remoto + module: vault-remote + args: + action: pull + key: db_password + remote_path: "/etc/shflow/vault" + become: true + + - name: Sincronizar todos los secretos locales al host remoto + module: vault-remote + args: + action: sync + remote_path: "/etc/shflow/vault" + become: true diff --git a/examples/wait.yaml b/examples/wait.yaml new file mode 100644 index 0000000..87c9bb4 --- /dev/null +++ b/examples/wait.yaml @@ -0,0 +1,5 @@ +tasks: + - name: Pausa breve + module: wait + args: + seconds: 1.5 diff --git a/examples/winremote_check_test.yaml b/examples/winremote_check_test.yaml new file mode 100644 index 0000000..518e73d --- /dev/null +++ b/examples/winremote_check_test.yaml @@ -0,0 +1,11 @@ +# Playbook: winremote_check_test.yml +# Descripción: Verifica acceso remoto a un equipo Windows mediante SSH y ejecución de PowerShell +# Autor: Luis GuLo + +tasks: + - name: Verificar acceso remoto a Windows + module: winremote_check + args: + winuser: guillermo + winpassword: "{{ vault('winpass') }}" + port: 2222 diff --git a/examples/winremote_detect.yaml b/examples/winremote_detect.yaml new file mode 100644 index 0000000..4eca690 --- /dev/null +++ b/examples/winremote_detect.yaml @@ -0,0 +1,8 @@ +tasks: + - name: Detectar protocolos remotos en equipo Windows + module: winremote_detect + args: + ssh_port: 22 + winrm_port: 5985 + + diff --git a/examples/winremote_exec.yaml b/examples/winremote_exec.yaml new file mode 100644 index 0000000..967f122 --- /dev/null +++ b/examples/winremote_exec.yaml @@ -0,0 +1,10 @@ +tasks: + + - name: Ejecutar comando remoto en Windows vía SSH + module: winremote_exec + args: + winuser: guillermo + winpassword: puertas + port: 2222 + #command: "Get-Process | Where-Object {$_.CPU -gt 1}" + command: "whoami" diff --git a/examples/winremote_exec_winrm.yml b/examples/winremote_exec_winrm.yml new file mode 100644 index 0000000..28e6afb --- /dev/null +++ b/examples/winremote_exec_winrm.yml @@ -0,0 +1,9 @@ +tasks: + + - name: Ejecutar comando remoto en Windows vía WinRM (WS-Manager) + module: winremote_exec_winrm + args: + winuser: guillermo + winpassword: puertas + command: "cmd.exe /c echo hola > C:\\salida.txt" + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3a9cdd9 --- /dev/null +++ b/install.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Module: install +# Description: Instalador de ShFlow en modo local o global +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.3.0 + +set -e + +# 🌐 Detectar idioma del sistema +LANGUAGE="es" +[[ "${LANG,,}" != *es* ]] && LANGUAGE="en" + +# 🗣️ Mensajes traducidos +declare -A tr + +if [[ "$LANGUAGE" == "es" ]]; then + tr[logo]="🐙 SHFLOW" + tr[mode]="🔧 Instalando ShFlow en modo: %s" + tr[folder]="📁 Carpeta de instalación: %s" + tr[prev_detected]="⚠️ Instalación previa detectada en %s" + tr[preserve_vault]="📦 Preservando vault existente..." + tr[preserve_inventory]="📦 Preservando inventory existente..." + tr[preserve_modules]="📦 Preservando user_modules existente..." + tr[removing_old]="🧹 Eliminando instalación previa..." + tr[copying]="📦 Copiando archivos..." + tr[restore_vault]="🔁 Restaurando vault..." + tr[restore_inventory]="🔁 Restaurando inventory..." + tr[restore_modules]="🔁 Restaurando user_modules..." + tr[env_added]="✅ Variables añadidas a %s" + tr[env_exists]="ℹ️ SHFLOW_HOME ya está definido en %s" + tr[done]="🎉 Instalación completada correctamente." + tr[installed]="📦 Proyecto instalado en: %s" + tr[symlinks]="🔗 Symlinks creados en: %s" + tr[restart]="🧠 Recuerda reiniciar tu terminal o ejecutar: source %s" + tr[run]="👉 Puedes ejecutar 'shflow' desde cualquier ruta del terminal." +else + tr[logo]="🐙 SHFLOW" + tr[mode]="🔧 Installing ShFlow in mode: %s" + tr[folder]="📁 Installation folder: %s" + tr[prev_detected]="⚠️ Previous installation detected at %s" + tr[preserve_vault]="📦 Preserving existing vault..." + tr[preserve_inventory]="📦 Preserving existing inventory..." + tr[preserve_modules]="📦 Preserving existing user_modules..." + tr[removing_old]="🧹 Removing previous installation..." + tr[copying]="📦 Copying files..." + tr[restore_vault]="🔁 Restoring vault..." + tr[restore_inventory]="🔁 Restoring inventory..." + tr[restore_modules]="🔁 Restoring user_modules..." + tr[env_added]="✅ Variables added to %s" + tr[env_exists]="ℹ️ SHFLOW_HOME already defined in %s" + tr[done]="🎉 Installation completed successfully." + tr[installed]="📦 Project installed at: %s" + tr[symlinks]="🔗 Symlinks created at: %s" + tr[restart]="🧠 Remember to restart your terminal or run: source %s" + tr[run]="👉 You can run 'shflow' from any terminal path." +fi + +# 🖼️ Logo +[[ -f "shflow-logo.ascii" ]] && cat shflow-logo.ascii || echo "${tr[logo]}" + +# 🧭 Detectar modo de instalación +if [[ "$EUID" -eq 0 ]]; then + INSTALL_DIR="/opt/shflow" + BIN_DIR="/usr/local/bin" + MODE="global" +else + INSTALL_DIR="$HOME/shflow" + BIN_DIR="$HOME/.local/bin" + MODE="local" +fi + +printf "${tr[mode]}\n" "$MODE" +printf "${tr[folder]}\n" "$INSTALL_DIR" + +mkdir -p "$INSTALL_DIR" "$BIN_DIR" + +# 🧹 Limpiar instalación previa +if [[ -d "$INSTALL_DIR" ]]; then + printf "${tr[prev_detected]}\n" "$INSTALL_DIR" + + [[ -d "$INSTALL_DIR/core/vault" ]] && echo "${tr[preserve_vault]}" && mv "$INSTALL_DIR/core/vault" /tmp/shflow_vault_backup + [[ -d "$INSTALL_DIR/core/inventory" ]] && echo "${tr[preserve_inventory]}" && mv "$INSTALL_DIR/core/inventory" /tmp/shflow_inventory_backup + [[ -d "$INSTALL_DIR/user_modules" ]] && echo "${tr[preserve_modules]}" && mv "$INSTALL_DIR/user_modules" /tmp/shflow_user_modules_backup + + echo "${tr[removing_old]}" + rm -rf "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR" +fi + +# 📥 Copiar archivos +echo "${tr[copying]}" +for file in shflow.sh shflow.tr.es shflow.tr.en vault.sh vault.tr.es vault.tr.en LICENSE README.md; do cp "$file" "$INSTALL_DIR/"; done +for dir in core community_modules user_modules examples; do cp -r "$dir" "$INSTALL_DIR/"; done + +# 🔁 Restaurar backups +[[ -d "/tmp/shflow_vault_backup" ]] && echo "${tr[restore_vault]}" && rm -rf "$INSTALL_DIR/core/vault" && mv /tmp/shflow_vault_backup "$INSTALL_DIR/core/vault" +[[ -d "/tmp/shflow_inventory_backup" ]] && echo "${tr[restore_inventory]}" && rm -rf "$INSTALL_DIR/core/inventory" && mv /tmp/shflow_inventory_backup "$INSTALL_DIR/core/inventory" +[[ -d "/tmp/shflow_user_modules_backup" ]] && echo "${tr[restore_modules]}" && rm -rf "$INSTALL_DIR/user_modules" && mv /tmp/shflow_user_modules_backup "$INSTALL_DIR/user_modules" + +# 🔗 Symlinks +ln -sf "$INSTALL_DIR/shflow.sh" "$BIN_DIR/shflow" +ln -sf "$INSTALL_DIR/vault.sh" "$BIN_DIR/shflow-vault" +ln -sf "$INSTALL_DIR/core/utils/shflow-doc.sh" "$BIN_DIR/shflow-doc" +ln -sf "$INSTALL_DIR/core/utils/module-docgen.sh" "$BIN_DIR/module-docgen" +ln -sf "$INSTALL_DIR/core/utils/shflow-check.sh" "$BIN_DIR/shflow-check" +ln -sf "$INSTALL_DIR/core/utils/shflow-trust.sh" "$BIN_DIR/shflow-trust" +ln -sf "$INSTALL_DIR/core/utils/shflow-ssh-init.sh" "$BIN_DIR/shflow-ssh-init" +ln -sf "$INSTALL_DIR/core/utils/vault-init.sh" "$BIN_DIR/vault-init" + +# 🧠 Variables de entorno +PROFILE_FILE="$HOME/.bashrc" +[[ "$SHELL" == *zsh ]] && PROFILE_FILE="$HOME/.zshrc" + +if ! grep -q "SHFLOW_HOME" "$PROFILE_FILE"; then + echo "export SHFLOW_HOME=\"$INSTALL_DIR\"" >> "$PROFILE_FILE" + echo "export PATH=\"\$PATH:$BIN_DIR\"" >> "$PROFILE_FILE" + printf "${tr[env_added]}\n" "$PROFILE_FILE" +else + printf "${tr[env_exists]}\n" "$PROFILE_FILE" +fi + +# ✅ Finalización +echo "" +echo "${tr[done]}" +printf "${tr[installed]}\n" "$INSTALL_DIR" +printf "${tr[symlinks]}\n" "$BIN_DIR" +printf "${tr[restart]}\n" "$PROFILE_FILE" +echo "${tr[run]}" diff --git a/shflow-logo.ascii b/shflow-logo.ascii new file mode 100644 index 0000000..a5f1df8 --- /dev/null +++ b/shflow-logo.ascii @@ -0,0 +1,18 @@ +[?25l▊    +▊ ┊▂▄▅▆▂▇▃▃▂▆▅▃▁  ▉  +▊ ▂▅▃▅▇┈ ▝▝┈ ┈▆▄▆▃┈  ▉  +▊ ▗▘▅▇┘ ▌▍ ┈▚▅▝▖ ▉ ▉  +▊ ▗▗▆▆▅▄╾▃┈ ▁▊▌▁ ┈▘▘ ▇▖▖    +▊ ▗▗▏ ▇▖▅▂▄▅▅▄▆▄▃▇ ▊▉┊ ╺▂▂▂▂▂▂▂▂▂▂▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅  +▊ ▍▍ ┈▘▗▃╾▃▃▅▄▁▖▎ ▗▏▏ ╴▄▆▁▁▆▄┊▊▇▌ ▅▅▅▅▅▅▏▅▅▏  +▊▉▏▁▃╼━━┉▖▉▏ ┈▌▋┊╴▘▘▝▃▃▃▅┊▉▏ ▊ ▝▁▁▝▅▏▊┊▌▅▆▅▖ ▎▉▇▇▇▇ ▎▉▏┈▃▄▅▄▂ ▗▄▏ ▄▄▖ ▗▄╴  +▊ ▏▎┈ ▍▊▁▚▅▇▖▆▆▇▂▃▃▂┈╶┈▘ ▅▄▃▂▇▅▏▊▗▗┈▇▏▉▏▎ ▇▇▇▌ ▎▉▏▍▉▘▇▖┊▖▝┊▌▗┊▖▋▗┊▌  +▊ ▍▌ ┈▘┊▚┈ ▗▘┈▄▇▗▅▅▖▇▅▂▅▁ ▋▆▃▂▁▘ ▍▊▌▌ ▉▏▉▏▎▉▏ ▎▉▏▎▉▖┈▋ ▌ ▍▉▘▗▝┊▘▉▏  +▊ ▝▉▖ ▗▗▇▖▝▖┈▎▊▌ ▉▏▝▘▊ ╴▅▄━╸ ▆▄▃▃▄▆ ▝▄▘ ▄▄▏▃▃▏ ▃▃▏┈▄▃▂▃▅┈ ▉▃▃▘ ▃▃▘  +▊ ▝▝▖▗▗ ┈▝▂▅▏▉▏ ▇▆▆┈ ┈┈╺┒▁ ▗▃▏ ▃▃▃▃▃▃▃▃▃▃▃▃ ▗▃  +▊ ▝▖▘┊ ┈▆▖ ▖ ▗▁▅━▃▇╺▖▍   +▊ ┈▄▝▃┈ ▝┈╴▄┈╶▃▝╾▃▆┕╸▁┗▖┗▌   +▏ ┈▅▂▅▃▁ ┈▖ ╵▇▍▇▅╾▃▆▚▁┗▏▘ ▍ ▇▇   +▏ ▇▅▄▂▆▅┊┈▗┊ ▇┓┊▌┈┈ ▝ ▗╺▄▄▄▄▄▄▄▄▄  +▏▄▄▄▄▄▄▄▄▄▅▄▄▄▅▇▇▇ ▅▄▄▄▄▇▄▄▄▄▄▄▅▏▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ ▆▆▆▆▆▆▆▆▆▆ +[?25h \ No newline at end of file diff --git a/shflow-logo.png b/shflow-logo.png new file mode 100644 index 0000000..8924473 Binary files /dev/null and b/shflow-logo.png differ diff --git a/shflow.sh b/shflow.sh new file mode 100755 index 0000000..f823f78 --- /dev/null +++ b/shflow.sh @@ -0,0 +1,308 @@ +#!/bin/bash +# ShFlow Playbook Runner +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.8.0 + +set -euo pipefail + +# 📁 Rutas clave +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +INVENTORY="$PROJECT_ROOT/core/inventory/hosts.yaml" +VAULT_DIR="$PROJECT_ROOT/core/vault" +VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}" + +# 🌐 Cargar render_msg y traducciones +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/shflow.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +# 🌀 Banner institucional +shflow_banner() { + local banner=$(grep -E '^# Version:' "$0" | sed 's/^# Version:/ShFlow version:/') + local padding=" " + echo "🌀 $banner$padding" +} +shflow_banner + +# 🔧 Verbosidad y variables +SHFLOW_VERBOSITY=1 +PLAYBOOK="" +HOST="" +GROUP="" +DEBUG=false +declare -A shflow_vars + +# 📣 Trazas condicionales +echolog() { + local level="$1"; shift + local message="$*" + local verbosity="${TASK_VERBOSITY:-$SHFLOW_VERBOSITY}" + [[ "$verbosity" -ge "$level" ]] && echo "$message" +} + +# 🔐 Resolución de secretos +resolve_vault_references() { + local input="$1" + local output="$input" + local pattern='\{\{\s*vault\((["'\''])([^"'\''\)]+)\1\)\s*\}\}' + while [[ "$output" =~ $pattern ]]; do + local full="${BASH_REMATCH[0]}" + local key="${BASH_REMATCH[2]}" + local secret="" + if [[ -f "$VAULT_DIR/$key.gpg" ]]; then + secret=$(gpg --quiet --batch --yes --passphrase-file "$VAULT_KEY" -d "$VAULT_DIR/$key.gpg" 2>/dev/null || true) + fi + output="${output//$full/$secret}" + done + echo "$output" +} + +# 🧠 Interpolación de argumentos +interpolate_args() { + local raw="$1" host="$2" label="$3" + local result="$raw" + result="$(resolve_vault_references "$result")" + result="${result//\{\{ name \}\}/$host}" + result="${result//\{\{ label \}\}/$label}" + for var in "${!shflow_vars[@]}"; do + safe_value="${shflow_vars[$var]}" + safe_value="${safe_value//$'\n'/\\n}" + safe_value="${safe_value//$'\r'/\\r}" + safe_value="${safe_value//$'\t'/\\t}" + safe_value="${safe_value//$'\0'/ }" + result="${result//\{\{ $var \}\}/$safe_value}" + done + echo "$result" +} + +# 🧪 Validación de argumentos +if [[ $# -eq 0 ]]; then + echo "${tr[no_args]:-❌ No se especificaron argumentos. Usa -f y -h o -g }" + exit 1 +fi + +while [[ $# -gt 0 ]]; do + case "$1" in + -f|--file) + [[ -z "${2:-}" || "${2:-}" == -* ]] && echo "${tr[missing_file]:-❌ Falta el nombre del archivo YAML tras $1}" && exit 1 + PLAYBOOK="$2"; shift 2 ;; + -h|--host) + [[ -z "${2:-}" || "${2:-}" == -* ]] && echo "${tr[missing_host]:-❌ Falta el nombre del host tras $1}" && exit 1 + HOST="$2"; shift 2 ;; + -g|--group) + [[ -z "${2:-}" || "${2:-}" == -* ]] && echo "${tr[missing_group]:-❌ Falta el nombre del grupo tras $1}" && exit 1 + GROUP="$2"; shift 2 ;; + --quiet) SHFLOW_VERBOSITY=0; shift ;; + --verbose) SHFLOW_VERBOSITY=2; shift ;; + --debug) SHFLOW_VERBOSITY=3; DEBUG=true; shift ;; + --version|version) + echo "$(render_msg "${tr[version_path]:-Ubicación: {path}" "path=$(realpath "$0")")" + exit 0 ;; + --help) + echo -e "${tr[help_header]:-ShFlow — Automatización ligera y extensible con Shell}\n" + echo "${tr[help_usage]:-Uso: shflow -f [-h | -g ] [opciones]}" + echo "" + echo "${tr[help_options]:-Opciones:}" + echo "${tr[help_opt_file]:- -f, --file Playbook YAML a ejecutar}" + echo "${tr[help_opt_host]:- -h, --host Host individual del inventario}" + echo "${tr[help_opt_group]:- -g, --group Grupo de hosts del inventario}" + echo "${tr[help_opt_quiet]:- --quiet Silencia toda salida excepto errores}" + echo "${tr[help_opt_verbose]:- --verbose Muestra trazas detalladas}" + echo "${tr[help_opt_debug]:- --debug Modo depuración con trazas internas}" + echo "${tr[help_opt_version]:- --version Muestra ubicación del ejecutable}" + echo "${tr[help_opt_help]:- --help Muestra esta ayuda}" + echo "" + echo "${tr[help_example]:-Ejemplo:}" + echo "${tr[help_example_cmd]:- shflow -f tareas.yaml -g servidores --verbose}" + exit 0 ;; + *) + $PROJECT_ROOT/core/utils/eg.sh "$@" + echo "$(render_msg "${tr[unknown_option]:-❌ Opción desconocida: {opt}}" "opt=$1")" + exit 1 ;; + esac +done + +# 📋 Validación de playbook +[ -z "$PLAYBOOK" ] && echo "${tr[no_playbook]:-❌ Playbook no especificado. Usa -f }" && exit 1 +[ ! -f "$PLAYBOOK" ] && echo "$(render_msg "${tr[playbook_not_found]:-❌ Playbook no encontrado: {file}}" "file=$PLAYBOOK")" && exit 1 + +TASKS_JSON=$(yq -r .tasks "$PLAYBOOK") +NUM_TASKS=$(echo "$TASKS_JSON" | jq 'length') +[ "$NUM_TASKS" -eq 0 ] && echo "${tr[no_tasks]:-❌ No se encontraron tareas en el playbook.}" && exit 1 + +# 🧠 Resolución de hosts +HOSTS=() +if [ -n "$HOST" ]; then + HOSTS+=("$HOST") +elif [ -n "$GROUP" ]; then + HOSTS_RAW=$(yq ".all.children.\"$GROUP\".hosts | keys | .[]" "$INVENTORY") + [ -z "$HOSTS_RAW" ] && echo "$(render_msg "${tr[group_not_found]:-❌ Grupo '{group}' no encontrado en el inventario.}" "group=$GROUP")" && exit 1 + while IFS= read -r line; do HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')"); done <<< "$HOSTS_RAW" +else + HOSTS_LINE=$(yq -r '.hosts // ""' "$PLAYBOOK") + if [ -z "$HOSTS_LINE" ]; then + HOSTGROUP=$(yq -r '.hostgroup // ""' "$PLAYBOOK") + if [ -n "$HOSTGROUP" ]; then + HOSTS_RAW=$(yq ".all.children.\"$HOSTGROUP\".hosts | keys | .[]" "$INVENTORY") + [ -z "$HOSTS_RAW" ] && echo "$(render_msg "${tr[group_not_found]:-❌ Grupo '{group}' no encontrado en el inventario.}" "group=$HOSTGROUP")" && exit 1 + while IFS= read -r line; do HOSTS+=("$(echo "$line" | sed 's/^\"\(.*\)\"$/\1/')"); done <<< "$HOSTS_RAW" + else + echo "${tr[no_host_specified]:-❌ No se especificó ningún host. Usa -h, -g, 'hosts:' o 'hostgroup:' en el playbook.}" + exit 1 + fi + else + IFS=',' read -ra HOSTS <<< "$HOSTS_LINE" + for i in "${!HOSTS[@]}"; do HOSTS[$i]=$(echo "${HOSTS[$i]}" | xargs); done + fi +fi + +# 📦 Carga de variables globales +GLOBAL_VARS="$PROJECT_ROOT/core/inventory/vars/all.yaml" +if [[ -f "$GLOBAL_VARS" ]]; then + GLOBAL_KEYS=$(yq -r 'keys[]' "$GLOBAL_VARS") + for key in $GLOBAL_KEYS; do + raw_value=$(yq -r ".\"$key\"" "$GLOBAL_VARS") + resolved_value="$(resolve_vault_references "$raw_value")" + shflow_vars["$key"]="$resolved_value" + done +fi + +# 📦 Carga de variables locales del playbook +VARS_KEYS=$(yq -r '.vars | keys[]' "$PLAYBOOK" 2>/dev/null || true) +for key in $VARS_KEYS; do + raw_value=$(yq -r ".vars.\"$key\"" "$PLAYBOOK") + resolved_value="$(resolve_vault_references "$raw_value")" + shflow_vars["$key"]="$resolved_value" +done + +# 👤 Usuario remoto +REMOTE_USER="${shflow_vars["remote_user"]:-$USER}" + +# 🚀 Ejecución por host +run_for_host() { + local CURRENT_HOST="$1" + local HOST_IP LABEL + local output_buffer=$(mktemp) + + { + HOST_IP=$(yq ".all.hosts.\"$CURRENT_HOST\".ansible_host" "$INVENTORY" | sed 's/^\"\(.*\)\"$/\1/') + LABEL=$(yq ".all.hosts.\"$CURRENT_HOST\".label" "$INVENTORY" | sed 's/^\"\(.*\)\"$/\1/') + [[ "$HOST_IP" == "null" || -z "$HOST_IP" ]] && HOST_IP="$CURRENT_HOST" + [[ "$LABEL" == "null" || -z "$LABEL" ]] && LABEL="$CURRENT_HOST" + + echolog 1 "$(render_msg "${tr[host_info]:-🔧 Host: {host} ({ip})}" "host=$CURRENT_HOST" "ip=$HOST_IP")" + echolog 2 "$(render_msg "${tr[ssh_user]:-👤 Usuario SSH: {user}}" "user=$REMOTE_USER")" + + # (Las tareas se ejecutarán en Parte 4) + } > "$output_buffer" 2>&1 + + echo -e "\n🖥️ Host: $CURRENT_HOST\n$(cat "$output_buffer")" + rm -f "$output_buffer" +} + + for ((i=0; i /dev/null && echo "$(render_msg "${tr[function_not_found]:-❌ Función '{function}' no encontrada en el módulo}" "function=${MODULE}_task")" && continue + + INTERPOLATED_ARGS="$(interpolate_args "$ARGS_RAW" "$CURRENT_HOST" "$LABEL")" + ARG_KEYS=$(echo "$INTERPOLATED_ARGS" | jq -r 'keys[]') + ARG_VALUES=() + for key in $ARG_KEYS; do + resolved=$(echo "$INTERPOLATED_ARGS" | jq -r ".[\"$key\"]") + resolved=$(echo "$resolved" | sed 's/^\"\(.*\)\"$/\1/') + ARG_VALUES+=("${key}=${resolved}") + done + + for extra_key in become; do + if [[ ! " ${ARG_KEYS[*]} " =~ " ${extra_key} " ]]; then + value="${shflow_vars[$extra_key]:-}" + [[ -n "$value" ]] && ARG_VALUES+=("${extra_key}=${value}") + fi + done + + local output exit_code + set +e + output=$("${MODULE}_task" "$REMOTE_USER@$HOST_IP" "${ARG_VALUES[@]}" 2>&1) + exit_code=$? + set -e + + [[ -n "$CAPTURE_LOG" ]] && shflow_vars["$CAPTURE_LOG"]="$output" + [[ -n "$REGISTER" ]] && shflow_vars["$REGISTER"]="$output" + [[ -n "$CAPTURE_ERR" ]] && shflow_vars["$CAPTURE_ERR"]="$exit_code" + + [[ -n "$CAPTURE_LOG" ]] && export "shflow_vars_${CAPTURE_LOG}=${shflow_vars[$CAPTURE_LOG]}" + [[ -n "$REGISTER" ]] && export "shflow_vars_${REGISTER}=${shflow_vars[$REGISTER]}" + [[ -n "$CAPTURE_ERR" ]] && export "shflow_vars_${CAPTURE_ERR}=${shflow_vars[$CAPTURE_ERR]}" + + echo "$output" + [ "$exit_code" -ne 0 ] && echo "$(render_msg "${tr[task_failed]:-⚠️ Tarea '{name}' falló en host '{host}'}" "name=$NAME" "host=$CURRENT_HOST")" + echo "" + done + } > "$output_buffer" 2>&1 + + echo -e "\n🖥️ Host: $CURRENT_HOST\n$(cat "$output_buffer")" + rm -f "$output_buffer" +} + +# ⚙️ Ejecución paralela o secuencial +if [[ "$PARALLELISM" == "true" ]]; then + for H in "${HOSTS[@]}"; do run_for_host "$H" & done + wait +else + for H in "${HOSTS[@]}"; do run_for_host "$H"; done +fi + +# 🧹 Cierre defensivo +return 0 2>/dev/null || true diff --git a/shflow.tr.en b/shflow.tr.en new file mode 100644 index 0000000..ff2c4f1 --- /dev/null +++ b/shflow.tr.en @@ -0,0 +1,32 @@ +no_args=❌ No arguments specified. Use -f and -h or -g +missing_file=❌ Missing YAML file name after {flag} +missing_host=❌ Missing host name after {flag} +missing_group=❌ Missing group name after {flag} +version_path=Location: {path} +help_header=ShFlow — Lightweight and extensible automation with Shell +help_usage=Usage: shflow -f [-h | -g ] [options] +help_options=Options: +help_opt_file= -f, --file YAML playbook to execute +help_opt_host= -h, --host Individual host from inventory +help_opt_group= -g, --group Group of hosts from inventory +help_opt_quiet= --quiet Silence all output except errors +help_opt_verbose= --verbose Show detailed traces +help_opt_debug= --debug Debug mode with internal traces +help_opt_version= --version Show executable location +help_opt_help= --help Show this help +help_example=Example: +help_example_cmd= shflow -f tasks.yaml -g servers --verbose +unknown_option=❌ Unknown option: {opt} +no_playbook=❌ No playbook specified. Use -f +playbook_not_found=❌ Playbook not found: {file} +no_tasks=❌ No tasks found in the playbook. +group_not_found=❌ Group '{group}' not found in inventory. +no_host_specified=❌ No host specified. Use -h, -g, 'hosts:' or 'hostgroup:' in the playbook. +module_not_found=❌ Module not found: {module}.sh in known paths +function_not_found=❌ Function '{function}' not found in module +task_skipped=⏭️ Task SKIPPED "{name}" due to condition: {condition} +task_running=🔧 Running task: "{name}" (module: "{module}") +task_failed=⚠️ Task '{name}' failed on host '{host}' +ssh_user=👤 SSH user: {user} +host_info=🔧 Host: {host} ({ip}) +condition_met=🔍 Condition met: {condition} diff --git a/shflow.tr.es b/shflow.tr.es new file mode 100644 index 0000000..2f6f642 --- /dev/null +++ b/shflow.tr.es @@ -0,0 +1,32 @@ +no_args=❌ No se especificaron argumentos. Usa -f y -h o -g +missing_file=❌ Falta el nombre del archivo YAML tras {flag} +missing_host=❌ Falta el nombre del host tras {flag} +missing_group=❌ Falta el nombre del grupo tras {flag} +version_path=Ubicación: {path} +help_header=ShFlow — Automatización ligera y extensible con Shell +help_usage=Uso: shflow -f [-h | -g ] [opciones] +help_options=Opciones: +help_opt_file= -f, --file Playbook YAML a ejecutar +help_opt_host= -h, --host Host individual del inventario +help_opt_group= -g, --group Grupo de hosts del inventario +help_opt_quiet= --quiet Silencia toda salida excepto errores +help_opt_verbose= --verbose Muestra trazas detalladas +help_opt_debug= --debug Modo depuración con trazas internas +help_opt_version= --version Muestra ubicación del ejecutable +help_opt_help= --help Muestra esta ayuda +help_example=Ejemplo: +help_example_cmd= shflow -f tareas.yaml -g servidores --verbose +unknown_option=❌ Opción desconocida: {opt} +no_playbook=❌ Playbook no especificado. Usa -f +playbook_not_found=❌ Playbook no encontrado: {file} +no_tasks=❌ No se encontraron tareas en el playbook. +group_not_found=❌ Grupo '{group}' no encontrado en el inventario. +no_host_specified=❌ No se especificó ningún host. Usa -h, -g, 'hosts:' o 'hostgroup:' en el playbook. +module_not_found=❌ Módulo no encontrado: {module}.sh en rutas conocidas +function_not_found=❌ Función '{function}' no encontrada en el módulo +task_skipped=⏭️ Tarea OMITIDA "{name}" por condición: {condition} +task_running=🔧 Ejecutando tarea: "{name}" (módulo: "{module}") +task_failed=⚠️ Tarea '{name}' falló en host '{host}' +ssh_user=👤 Usuario SSH: {user} +host_info=🔧 Host: {host} ({ip}) +condition_met=🔍 Condición cumplida: {condition} diff --git a/user_modules/.gitignore b/user_modules/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/vault.sh b/vault.sh new file mode 100755 index 0000000..cddeb92 --- /dev/null +++ b/vault.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# ShFlow Vault Manager +# License: GPLv3 +# Author: Luis GuLo +# Version: 1.5.1 +# Dependencies: gpg + +set -euo pipefail + +# 🧭 Detección de la raíz del proyecto +PROJECT_ROOT="${SHFLOW_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" + +# 📁 Rutas clave +VAULT_DIR="$PROJECT_ROOT/core/vault" +VAULT_KEY="${VAULT_KEY:-$HOME/.shflow.key}" +VAULT_PUBKEY="${VAULT_PUBKEY:-$HOME/.shflow.pub}" +VAULT_RECIPIENT="${VAULT_RECIPIENT:-}" + +# 🧩 Cargar render_msg si no está disponible +COMMON_LIB="$PROJECT_ROOT/core/lib/translate_msg.sh" +if ! declare -f render_msg &>/dev/null; then + [[ -f "$COMMON_LIB" ]] && source "$COMMON_LIB" +fi + +# 🌐 Cargar traducciones +lang="${SHFLOW_LANG:-es}" +trfile="$PROJECT_ROOT/vault.tr.${lang}" +declare -A tr +if [[ -f "$trfile" ]]; then while IFS='=' read -r k v; do tr["$k"]="$v"; done < "$trfile"; fi + +encrypt_secret() { + local key="$1" + local value="$2" + + if [ -f "$VAULT_PUBKEY" ]; then + echo "$(render_msg "${tr[encrypt_asym]:-🔐 Usando cifrado asimétrico para '{key}'}" "key=$key")" + echo "$value" | gpg --encrypt --armor --batch --yes --recipient "$VAULT_RECIPIENT" -o "$VAULT_DIR/$key.gpg" + elif [ -f "$VAULT_KEY" ]; then + echo "$(render_msg "${tr[encrypt_sym]:-🔐 Usando cifrado simétrico para '{key}'}" "key=$key")" + echo "$value" | gpg --symmetric --batch --yes --passphrase-file "$VAULT_KEY" -o "$VAULT_DIR/$key.gpg" + else + echo "${tr[missing_key]:-❌ No se encontró clave para cifrar. Ejecuta vault-init.sh primero.}" + return 1 + fi + + echo "$(render_msg "${tr[secret_saved]:-✅ Secreto '{key}' guardado en {dir}}" "key=$key" "dir=$VAULT_DIR")" +} + +decrypt_secret() { + local key="$1" + gpg --quiet --batch --yes --passphrase-file "$VAULT_KEY" -d "$VAULT_DIR/$key.gpg" 2>/dev/null || \ + gpg --quiet --batch --yes -d "$VAULT_DIR/$key.gpg" +} + +list_secrets() { + ls "$VAULT_DIR"/*.gpg 2>/dev/null | sed 's|.*/\(.*\)\.gpg|\1|' +} + +secret_exists() { + local key="$1" + [[ -f "$VAULT_DIR/$key.gpg" ]] +} + +remove_secret() { + local key="$1" + rm -f "$VAULT_DIR/$key.gpg" && echo "$(render_msg "${tr[secret_removed]:-🗑️ Secreto '{key}' eliminado.}" "key=$key")" +} + +edit_secret() { + local key="$1" + local current + current=$(decrypt_secret "$key") + read -s -p "$(render_msg "${tr[edit_prompt]:-🔑 Nuevo valor para '{key}':}" "key=$key")" new_value + echo "" + encrypt_secret "$key" "$new_value" +} + +export_secrets() { + for file in "$VAULT_DIR"/*.gpg; do + varname="$(basename "$file" .gpg)" + value="$(decrypt_secret "$varname")" + echo "export $varname=\"$value\"" + done +} + +vault_task() { + local host="$1"; shift + declare -A args + for arg in "$@"; do + key="${arg%%=*}" + value="${arg#*=}" + args["$key"]="$value" + done + + local action="${args[action]:-}" + local key="${args[key]:-}" + local value="${args[value]:-}" + local become="${args[become]:-}" + local prefix="" + [ "$become" = "true" ] && prefix="sudo" + + case "$action" in + get|show) decrypt_secret "$key" ;; + add) encrypt_secret "$key" "$value" ;; + edit) edit_secret "$key" ;; + remove) remove_secret "$key" ;; + exists) secret_exists "$key" ;; + list) list_secrets ;; + export) export_secrets ;; + *) echo "$(render_msg "${tr[action_invalid]:-❌ [vault] Acción '{action}' no soportada.}" "action=$action")"; return 1 ;; + esac +} + +check_dependencies_vault() { + if ! command -v gpg &> /dev/null; then + echo "${tr[missing_dep]:-❌ [vault] gpg no está disponible.}" + return 1 + fi + echo "${tr[dep_ok]:-✅ [vault] gpg disponible.}" + return 0 +} + +main() { + case "${1:-}" in + add) + read -s -p "$(render_msg "${tr[cli_prompt]:-🔑 Valor para '{key}':}" "key=$2")" value + echo "" + encrypt_secret "$2" "$value" + ;; + get|show) decrypt_secret "$2" ;; + edit) edit_secret "$2" ;; + remove) remove_secret "$2" ;; + list) list_secrets ;; + export) export_secrets ;; + exists) + secret_exists "$2" && echo "${tr[exists]:-✅ Existe}" || echo "${tr[not_exists]:-❌ No existe}" + ;; + *) + echo "${tr[usage]:-Uso: vault.sh {add|get|show|edit|remove|list|export|exists} }" + ;; + esac +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/vault.tr.en b/vault.tr.en new file mode 100644 index 0000000..6588c64 --- /dev/null +++ b/vault.tr.en @@ -0,0 +1,13 @@ +encrypt_asym=🔐 Using asymmetric encryption for '{key}' +encrypt_sym=🔐 Using symmetric encryption for '{key}' +missing_key=❌ No key found for encryption. Run vault-init.sh first. +secret_saved=✅ Secret '{key}' saved in {dir} +secret_removed=🗑️ Secret '{key}' removed. +edit_prompt=🔑 New value for '{key}': +cli_prompt=🔑 Value for '{key}': +exists=✅ Exists +not_exists=❌ Does not exist +usage=Usage: vault.sh {add|get|show|edit|remove|list|export|exists} +action_invalid=❌ [vault] Action '{action}' not supported. +missing_dep=❌ [vault] gpg is not available. +dep_ok=✅ [vault] gpg is available. diff --git a/vault.tr.es b/vault.tr.es new file mode 100644 index 0000000..2db6f47 --- /dev/null +++ b/vault.tr.es @@ -0,0 +1,13 @@ +encrypt_asym=🔐 Usando cifrado asimétrico para '{key}' +encrypt_sym=🔐 Usando cifrado simétrico para '{key}' +missing_key=❌ No se encontró clave para cifrar. Ejecuta vault-init.sh primero. +secret_saved=✅ Secreto '{key}' guardado en {dir} +secret_removed=🗑️ Secreto '{key}' eliminado. +edit_prompt=🔑 Nuevo valor para '{key}': +cli_prompt=🔑 Valor para '{key}': +exists=✅ Existe +not_exists=❌ No existe +usage=Uso: vault.sh {add|get|show|edit|remove|list|export|exists} +action_invalid=❌ [vault] Acción '{action}' no soportada. +missing_dep=❌ [vault] gpg no está disponible. +dep_ok=✅ [vault] gpg disponible. -- cgit v1.2.3