;;; 07-linuxplus-update-files.el --- linuxplus-update-files ;;; ;;; Provide safe helpers for updating existing project files. ;;; ;;; This file is part of the Linux+ Learning project. ;;; ;;; Copyright (c) 2026, Scott C. MacCallum (scm@sdf.org). ;;; ;;; This program is free software: you can redistribute it and/or modify ;;; it under the terms of the GNU Affero 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 Affero General Public License for more details. ;;; ;;; You should have received a copy of the GNU Affero General ;;; Public License along with this program. If not, see ;;; . ;;; Commentary: ;; This file provides small update helpers for existing project files. ;; ;; The main command is: ;; ;; M-x linuxplus-apply-firewall-score-update ;; ;; It updates STUDY.md by marking the firewall review section as Solid ;; and by recording the 20/20 firewall quiz score. ;; ;; The helper functions are intentionally simple. They are meant to ;; avoid fragile hand-written patch hunks when making repeated project ;; documentation updates. ;;; Code: (require 'subr-x) (defvar linuxplus-update-base-directory (file-name-directory (or load-file-name buffer-file-name default-directory)) "Base directory for Linux+ project update helpers.") (defun linuxplus-update--file (name) "Return the full project path for file NAME." (expand-file-name name linuxplus-update-base-directory)) (defun linuxplus-update--read-file (file) "Return the contents of FILE as a string." (with-temp-buffer (insert-file-contents file) (buffer-string))) (defun linuxplus-update--write-file (file text) "Write TEXT to FILE." (with-temp-file file (insert text))) (defun linuxplus-update--backup-file (file) "Create a numbered backup copy of FILE." (let* ((index 1) (backup (format "%s.bak.%d" file index))) (while (file-exists-p backup) (setq index (1+ index)) (setq backup (format "%s.bak.%d" file index))) (copy-file file backup) backup)) (defun linuxplus-update--section-bounds (text heading) "Return section bounds for HEADING in TEXT. The result is a cons cell whose car is the start position and whose cdr is the end position. The end is the next Markdown level two heading or the end of TEXT." (let ((start (string-match (concat "^" (regexp-quote heading) "$") text))) (unless start (error "Could not find heading: %s" heading)) (let* ((search-start (+ start (length heading))) (next (string-match "^## " text search-start))) (cons start (or next (length text)))))) (defun linuxplus-update-replace-section (file heading new-section) "In FILE, replace Markdown section HEADING with NEW-SECTION." (let* ((text (linuxplus-update--read-file file)) (bounds (linuxplus-update--section-bounds text heading)) (start (car bounds)) (end (cdr bounds)) (backup (linuxplus-update--backup-file file)) (updated (concat (substring text 0 start) (string-trim-right new-section) "\n\n" (substring text end)))) (linuxplus-update--write-file file updated) (message "Updated %s; backup saved as %s" file backup))) (defun linuxplus-update-insert-before-heading (file heading text-to-insert) "In FILE, insert TEXT-TO-INSERT before Markdown HEADING." (let* ((text (linuxplus-update--read-file file)) (marker (concat "\n" heading)) (pos (string-match (regexp-quote marker) text)) (backup (linuxplus-update--backup-file file))) (unless pos (error "Could not find heading: %s" heading)) (linuxplus-update--write-file file (concat (substring text 0 (1+ pos)) (string-trim-right text-to-insert) "\n\n" (substring text (1+ pos)))) (message "Updated %s; backup saved as %s" file backup))) (defun linuxplus-update-replace-section-with-region (file heading beg end) "Replace Markdown section HEADING in FILE with region BEG to END." (interactive (list (read-file-name "File to update: " linuxplus-update-base-directory) (read-string "Section heading: ") (region-beginning) (region-end))) (linuxplus-update-replace-section file heading (buffer-substring-no-properties beg end))) (defun linuxplus-update-insert-region-before-heading (file heading beg end) "Insert region BEG to END before Markdown HEADING in FILE." (interactive (list (read-file-name "File to update: " linuxplus-update-base-directory) (read-string "Insert before heading: ") (region-beginning) (region-end))) (linuxplus-update-insert-before-heading file heading (buffer-substring-no-properties beg end))) (defun linuxplus-update--firewall-section () "Return the corrected Firewall Review STUDY.md section." "## Weak Area: Firewall Review Missed area: Firewall basics, stateful firewall behavior, firewalld persistence, UFW command syntax, and DenyHosts/Fail2Ban recognition Correct concept: Firewalls use ACLs, inspect network packets, and use configuration files for persistence. ACL means access control list. Firewall ACL rules allow or deny traffic based on details such as source address, destination address, protocol, port, or interface. A stateless firewall checks packets against rules without remembering the state of a connection. A stateful firewall tracks connection state. For this book review, remember that a stateful firewall can operate faster for established connections and can determine if packets are fragmented. firewalld can use runtime configuration and permanent configuration. After testing firewalld runtime rules, make them permanent with: sudo firewall-cmd --runtime-to-permanent To view numbered UFW rules, use: sudo ufw status numbered To block SSH using simple UFW syntax, use: sudo ufw deny 22/tcp For this book item, remember DenyHosts and Fail2Ban as IDS tools. The expected answer associates them with /etc/hosts.deny. Exam wording trap: \"Make firewalld runtime rules permanent\" points to: firewall-cmd --runtime-to-permanent \"Numbered UFW rules\" points to: sudo ufw status numbered \"Deny SSH with simple UFW syntax\" points to: sudo ufw deny 22/tcp DenyHosts is strongly associated with /etc/hosts.deny. Fail2Ban commonly blocks through firewall actions in real-world configurations, but for the book question, memorize the book's expected wording. Commands/files to remember: sudo firewall-cmd --runtime-to-permanent sudo ufw status numbered sudo ufw deny 22/tcp /etc/hosts.deny Future lesson topic: 104-firewall-review-lesson.el 105-firewall-review-quiz.el Status: Solid") (defun linuxplus-update--firewall-score-note () "Return the firewall score note for STUDY.md." "Date: 2026-07-09 Lesson: 104-firewall-review-lesson.el Quiz: 105-firewall-review-quiz.el Score: 20/20 Missed: None Next action: Do a quick review later to keep the commands fresh: sudo firewall-cmd --runtime-to-permanent sudo ufw status numbered sudo ufw deny 22/tcp /etc/hosts.deny") (defun linuxplus-apply-firewall-score-update () "Update STUDY.md with the firewall review score and Solid status." (interactive) (let ((study-file (linuxplus-update--file "STUDY.md"))) (linuxplus-update-replace-section study-file "## Weak Area: Firewall Review" (linuxplus-update--firewall-section)) (let ((text (linuxplus-update--read-file study-file))) (unless (string-match-p "Score:\n\n 20/20" text) (linuxplus-update-insert-before-heading study-file "## Current Notes" (linuxplus-update--firewall-score-note))))) (message "Firewall review score update complete.")) (defun linuxplus-update-help () "Show help for Linux+ project update helpers." (interactive) (with-help-window "*Linux+ Update Help*" (princ "Linux+ project update helpers\n\n") (princ "Useful commands:\n\n") (princ " M-x linuxplus-apply-firewall-score-update\n") (princ " Update STUDY.md for the 20/20 firewall quiz score.\n\n") (princ " M-x linuxplus-update-replace-section-with-region\n") (princ " Replace a Markdown section with selected text.\n\n") (princ " M-x linuxplus-update-insert-region-before-heading\n") (princ " Insert selected text before a Markdown heading.\n\n") (princ "Each update creates a numbered .bak.N backup first.\n"))) (provide '07-linuxplus-update-files) ;;; 07-linuxplus-update-files.el ends here